From b276ce1258c59e476ec46d453a26c326ec96d102 Mon Sep 17 00:00:00 2001 From: Frozen FIsh <76603360+sudongyuer@users.noreply.github.com> Date: Sat, 4 Mar 2023 19:36:58 +0800 Subject: [PATCH] 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 --- packages/app/client/src/plugins/charts.ts | 1 + .../schema-component/antd/g2plot/G2Plot.tsx | 7 +- .../src/schema-component/antd/tabs/Tabs.tsx | 1 + .../schema-initializer/SchemaInitializer.tsx | 6 +- .../buttons/BlockInitializers.tsx | 109 ------ .../src/schema-initializer/items/index.tsx | 10 +- packages/core/database/src/database.ts | 8 +- packages/plugins/charts/client.d.ts | 4 + packages/plugins/charts/client.js | 30 ++ packages/plugins/charts/package.json | 13 + packages/plugins/charts/server.d.ts | 4 + packages/plugins/charts/server.js | 30 ++ .../charts/src/client/ChartBlockEngine.tsx | 120 +++++++ .../src/client/ChartBlockEngineDesigner.tsx | 238 +++++++++++++ .../src/client/ChartBlockInitializer.tsx | 216 ++++++++++++ .../src/client/ChartQueryBlockInitializer.tsx | 136 ++++++++ .../src/client/ChartQueryMetadataProvider.tsx | 62 ++++ .../charts/src/client/DataSetPreviewTable.tsx | 73 ++++ packages/plugins/charts/src/client/Icons.tsx | 99 ++++++ .../src/client/chartRenderComponents/index.ts | 7 + .../plugins/charts/src/client/hooks/index.ts | 19 ++ packages/plugins/charts/src/client/index.tsx | 101 ++++++ .../plugins/charts/src/client/locale/en-US.ts | 23 ++ .../plugins/charts/src/client/locale/index.ts | 18 + .../plugins/charts/src/client/locale/ja-JP.ts | 1 + .../plugins/charts/src/client/locale/ru-RU.ts | 1 + .../plugins/charts/src/client/locale/tr-TR.ts | 1 + .../plugins/charts/src/client/locale/zh-CN.ts | 63 ++++ .../charts/src/client/select/CustomSelect.tsx | 127 +++++++ .../charts/src/client/select/ReadPretty.tsx | 36 ++ .../plugins/charts/src/client/select/index.md | 38 +++ .../plugins/charts/src/client/select/index.ts | 2 + .../charts/src/client/select/shared.ts | 36 ++ .../src/client/settings/AddNewQuery.tsx | 161 +++++++++ .../src/client/settings/ConfigureFields.tsx | 18 + .../src/client/settings/QueriesTable.tsx | 26 ++ .../charts/src/client/settings/queryTypes.ts | 64 ++++ .../client/settings/schemas/chartsQueries.ts | 319 ++++++++++++++++++ .../src/client/templates/AreaTemplate.tsx | 64 ++++ .../src/client/templates/BarTemplate.tsx | 80 +++++ .../src/client/templates/ColumnTemplate.tsx | 81 +++++ .../src/client/templates/FunnelTemplate.tsx | 65 ++++ .../src/client/templates/LineTemplate.tsx | 72 ++++ .../src/client/templates/PieTemplate.tsx | 68 ++++ .../src/client/templates/RadarTemplate.tsx | 71 ++++ .../src/client/templates/ScatterTemplate.tsx | 90 +++++ .../src/client/templates/TableTemplate.tsx | 48 +++ .../charts/src/client/templates/index.ts | 21 ++ packages/plugins/charts/src/client/utils.ts | 39 +++ packages/plugins/charts/src/index.ts | 1 + .../src/server/actions/chartsQueries.ts | 44 +++ .../charts/src/server/actions/index.ts | 0 .../charts/src/server/collections/.gitkeep | 0 .../src/server/collections/chartsQueries.ts | 24 ++ packages/plugins/charts/src/server/index.ts | 1 + packages/plugins/charts/src/server/plugin.ts | 61 ++++ packages/plugins/charts/src/server/query.ts | 39 +++ .../plugins/charts/src/server/shared/index.ts | 2 + packages/presets/nocobase/package.json | 1 + packages/presets/nocobase/src/index.ts | 1 + packages/samples/hello/src/client/index.tsx | 2 +- yarn.lock | 25 +- 62 files changed, 3004 insertions(+), 124 deletions(-) create mode 100644 packages/app/client/src/plugins/charts.ts create mode 100755 packages/plugins/charts/client.d.ts create mode 100755 packages/plugins/charts/client.js create mode 100644 packages/plugins/charts/package.json create mode 100755 packages/plugins/charts/server.d.ts create mode 100755 packages/plugins/charts/server.js create mode 100644 packages/plugins/charts/src/client/ChartBlockEngine.tsx create mode 100644 packages/plugins/charts/src/client/ChartBlockEngineDesigner.tsx create mode 100644 packages/plugins/charts/src/client/ChartBlockInitializer.tsx create mode 100644 packages/plugins/charts/src/client/ChartQueryBlockInitializer.tsx create mode 100644 packages/plugins/charts/src/client/ChartQueryMetadataProvider.tsx create mode 100644 packages/plugins/charts/src/client/DataSetPreviewTable.tsx create mode 100644 packages/plugins/charts/src/client/Icons.tsx create mode 100644 packages/plugins/charts/src/client/chartRenderComponents/index.ts create mode 100644 packages/plugins/charts/src/client/hooks/index.ts create mode 100644 packages/plugins/charts/src/client/index.tsx create mode 100644 packages/plugins/charts/src/client/locale/en-US.ts create mode 100644 packages/plugins/charts/src/client/locale/index.ts create mode 100644 packages/plugins/charts/src/client/locale/ja-JP.ts create mode 100644 packages/plugins/charts/src/client/locale/ru-RU.ts create mode 100644 packages/plugins/charts/src/client/locale/tr-TR.ts create mode 100644 packages/plugins/charts/src/client/locale/zh-CN.ts create mode 100644 packages/plugins/charts/src/client/select/CustomSelect.tsx create mode 100644 packages/plugins/charts/src/client/select/ReadPretty.tsx create mode 100644 packages/plugins/charts/src/client/select/index.md create mode 100644 packages/plugins/charts/src/client/select/index.ts create mode 100644 packages/plugins/charts/src/client/select/shared.ts create mode 100644 packages/plugins/charts/src/client/settings/AddNewQuery.tsx create mode 100644 packages/plugins/charts/src/client/settings/ConfigureFields.tsx create mode 100644 packages/plugins/charts/src/client/settings/QueriesTable.tsx create mode 100644 packages/plugins/charts/src/client/settings/queryTypes.ts create mode 100644 packages/plugins/charts/src/client/settings/schemas/chartsQueries.ts create mode 100644 packages/plugins/charts/src/client/templates/AreaTemplate.tsx create mode 100644 packages/plugins/charts/src/client/templates/BarTemplate.tsx create mode 100644 packages/plugins/charts/src/client/templates/ColumnTemplate.tsx create mode 100644 packages/plugins/charts/src/client/templates/FunnelTemplate.tsx create mode 100644 packages/plugins/charts/src/client/templates/LineTemplate.tsx create mode 100644 packages/plugins/charts/src/client/templates/PieTemplate.tsx create mode 100644 packages/plugins/charts/src/client/templates/RadarTemplate.tsx create mode 100644 packages/plugins/charts/src/client/templates/ScatterTemplate.tsx create mode 100644 packages/plugins/charts/src/client/templates/TableTemplate.tsx create mode 100644 packages/plugins/charts/src/client/templates/index.ts create mode 100644 packages/plugins/charts/src/client/utils.ts create mode 100644 packages/plugins/charts/src/index.ts create mode 100644 packages/plugins/charts/src/server/actions/chartsQueries.ts create mode 100644 packages/plugins/charts/src/server/actions/index.ts create mode 100644 packages/plugins/charts/src/server/collections/.gitkeep create mode 100644 packages/plugins/charts/src/server/collections/chartsQueries.ts create mode 100644 packages/plugins/charts/src/server/index.ts create mode 100644 packages/plugins/charts/src/server/plugin.ts create mode 100644 packages/plugins/charts/src/server/query.ts create mode 100644 packages/plugins/charts/src/server/shared/index.ts diff --git a/packages/app/client/src/plugins/charts.ts b/packages/app/client/src/plugins/charts.ts new file mode 100644 index 000000000..f2115a4f1 --- /dev/null +++ b/packages/app/client/src/plugins/charts.ts @@ -0,0 +1 @@ +export { default } from '@nocobase/plugin-charts/client'; \ No newline at end of file diff --git a/packages/core/client/src/schema-component/antd/g2plot/G2Plot.tsx b/packages/core/client/src/schema-component/antd/g2plot/G2Plot.tsx index d58be137a..9cd945680 100644 --- a/packages/core/client/src/schema-component/antd/g2plot/G2Plot.tsx +++ b/packages/core/client/src/schema-component/antd/g2plot/G2Plot.tsx @@ -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 (props: ReactG2PlotProps, ref: any) { const { className, plot, config } = props; - const containerRef = useRef(undefined); const plotRef = useRef(undefined); @@ -107,7 +106,7 @@ export const G2PlotRenderer = forwardRef(function (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 (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
{t('In configuration')}...
; } diff --git a/packages/core/client/src/schema-component/antd/tabs/Tabs.tsx b/packages/core/client/src/schema-component/antd/tabs/Tabs.tsx index fb9b861c1..3309ac4c6 100644 --- a/packages/core/client/src/schema-component/antd/tabs/Tabs.tsx +++ b/packages/core/client/src/schema-component/antd/tabs/Tabs.tsx @@ -15,6 +15,7 @@ export const Tabs: any = observer((props: TabsProps) => { return ( { key={item.key} onClick={(info) => { item?.clearKeywords?.(); - onClick({ ...info, item }); + if (item.onClick) { + item.onClick({ ...info, item }); + } else { + onClick({ ...info, item }); + } }} > {compile(item.title)} diff --git a/packages/core/client/src/schema-initializer/buttons/BlockInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/BlockInitializers.tsx index c7f3b0b46..55daaa6db 100644 --- a/packages/core/client/src/schema-initializer/buttons/BlockInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/BlockInitializers.tsx @@ -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', diff --git a/packages/core/client/src/schema-initializer/items/index.tsx b/packages/core/client/src/schema-initializer/items/index.tsx index 3c8843a15..d844fe7c8 100644 --- a/packages/core/client/src/schema-initializer/items/index.tsx +++ b/packages/core/client/src/schema-initializer/items/index.tsx @@ -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'; diff --git a/packages/core/database/src/database.ts b/packages/core/database/src/database.ts index cfa231b5c..0dfb34c91 100644 --- a/packages/core/database/src/database.ts +++ b/packages/core/database/src/database.ts @@ -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 diff --git a/packages/plugins/charts/client.d.ts b/packages/plugins/charts/client.d.ts new file mode 100755 index 000000000..765db9222 --- /dev/null +++ b/packages/plugins/charts/client.d.ts @@ -0,0 +1,4 @@ +// @ts-nocheck +export * from './lib/client'; +export { default } from './lib/client'; + diff --git a/packages/plugins/charts/client.js b/packages/plugins/charts/client.js new file mode 100755 index 000000000..238820257 --- /dev/null +++ b/packages/plugins/charts/client.js @@ -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]; + } + }); +}); diff --git a/packages/plugins/charts/package.json b/packages/plugins/charts/package.json new file mode 100644 index 000000000..fe1a1a82e --- /dev/null +++ b/packages/plugins/charts/package.json @@ -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" + } +} diff --git a/packages/plugins/charts/server.d.ts b/packages/plugins/charts/server.d.ts new file mode 100755 index 000000000..e70edb928 --- /dev/null +++ b/packages/plugins/charts/server.d.ts @@ -0,0 +1,4 @@ +// @ts-nocheck +export * from './lib/server'; +export { default } from './lib/server'; + diff --git a/packages/plugins/charts/server.js b/packages/plugins/charts/server.js new file mode 100755 index 000000000..d06a7eb92 --- /dev/null +++ b/packages/plugins/charts/server.js @@ -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]; + } + }); +}); diff --git a/packages/plugins/charts/src/client/ChartBlockEngine.tsx b/packages/plugins/charts/src/client/ChartBlockEngine.tsx new file mode 100644 index 000000000..e4f390d4a --- /dev/null +++ b/packages/plugins/charts/src/client/ChartBlockEngine.tsx @@ -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 ( + <> + May be this chart block's query data has been deleted,please check!} /> + + ); + } + + const [currentConfig, setCurrentConfig] = useState({} 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 ? : }; + } + } + 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 ( + <> + + + ); +}; + +ChartBlockEngine.Designer = ChartBlockEngineDesigner; + +export { ChartBlockEngine }; diff --git a/packages/plugins/charts/src/client/ChartBlockEngineDesigner.tsx b/packages/plugins/charts/src/client/ChartBlockEngineDesigner.tsx new file mode 100644 index 000000000..1a0c4eb35 --- /dev/null +++ b/packages/plugins/charts/src/client/ChartBlockEngineDesigner.tsx @@ -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 ( + + {lang('Json config references: ')} + + {lang(title)} + + + ); +}; + +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 ( + + + + + + ); +}; + +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 ( + { + FormDialog( + { + okText: compile('{{t("Submit")}}'), + title: lang('Edit chart block'), + width: 1200, + bodyStyle: { background: '#f0f2f5', maxHeight: '65vh', overflow: 'auto' }, + }, + (form) => { + const [chartBlockEngineMetaData, setChartBlockEngineMetaData] = useState(null); + useEffect(() => { + const chartBlockEngineMetaData = { + query: { + id: query?.id, + }, + chart: form.values, //TODO + }; + setChartBlockEngineMetaData(chartBlockEngineMetaData); + }, [form.values.type]); + return ( + + +
+ {/* left*/} + + + { + return { + title: template.title, + key: template.type, + description: template.description, + group: template.group, + iconId: template.iconId, + }; + }), + }, + options: { + type: 'void', + 'x-component': 'Options', + }, + }, + }} + /> + + + {/* right*/} +
+ + {/* Chart Preview*/} + {chartBlockEngineMetaData && ( + <> + + + )} + + + {/*Data preview*/} + {chartBlockEngineMetaData?.query?.id && ( + + )} + +
+
+
+
+ ); + }, + ) + .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')} +
+ ); +}; diff --git a/packages/plugins/charts/src/client/ChartBlockInitializer.tsx b/packages/plugins/charts/src/client/ChartBlockInitializer.tsx new file mode 100644 index 000000000..efa89fb04 --- /dev/null +++ b/packages/plugins/charts/src/client/ChartBlockInitializer.tsx @@ -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(); + const field = useField(); + 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 ; +}); + +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 ( + { + 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(); + const [chartBlockEngineMetaData, setChartBlockEngineMetaData] = useState(null); + useEffect(() => { + const chartBlockEngineMetaData = { + query: { + id: chartQueryMetadata?.id, + }, + chart: form.values, //TODO + }; + setChartBlockEngineMetaData(chartBlockEngineMetaData); + }, [form.values.type]); + return ( + + +
+ {/* left*/} + + + + { + return { + title: template.title, + key: template.type, + description: template.description, + group: template.group, + iconId: template.iconId, + }; + }), + }, + options: { + type: 'void', + 'x-component': 'Options', + }, + }, + }} + /> + + + + {/* right*/} +
+ + {/* Chart Preview*/} + {chartBlockEngineMetaData && ( + <> + + + )} + + + {/*Data preview*/} + {chartBlockEngineMetaData?.query?.id && ( + + )} + +
+
+
+
+ ); + }, + ).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, + }, + }); + } + }} + /> + ); +}; diff --git a/packages/plugins/charts/src/client/ChartQueryBlockInitializer.tsx b/packages/plugins/charts/src/client/ChartQueryBlockInitializer.tsx new file mode 100644 index 000000000..d3f1048fa --- /dev/null +++ b/packages/plugins/charts/src/client/ChartQueryBlockInitializer.tsx @@ -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 ( +
+ + + + + +
+ ); + }, + ) + .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 ( + } + {...others} + onClick={async ({ item }) => { + onCreateBlockSchema({ item }); + setVisible(false); + }} + items={items} + /> + ); +}; diff --git a/packages/plugins/charts/src/client/ChartQueryMetadataProvider.tsx b/packages/plugins/charts/src/client/ChartQueryMetadataProvider.tsx new file mode 100644 index 000000000..ede292f2c --- /dev/null +++ b/packages/plugins/charts/src/client/ChartQueryMetadataProvider.tsx @@ -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 ; + } + + return ( + + {props.children} + + ); +}; + +export const useChartQueryMetadataContext = () => { + return useContext(ChartQueryMetadataContext); +}; diff --git a/packages/plugins/charts/src/client/DataSetPreviewTable.tsx b/packages/plugins/charts/src/client/DataSetPreviewTable.tsx new file mode 100644 index 000000000..ecb54b145 --- /dev/null +++ b/packages/plugins/charts/src/client/DataSetPreviewTable.tsx @@ -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 ( + <> + May be this chart block's query data has been deleted,please check!} /> + + ); + } + + if (loading) + return ( + <> + + + ); + return ( + + + + + + ); +}; diff --git a/packages/plugins/charts/src/client/Icons.tsx b/packages/plugins/charts/src/client/Icons.tsx new file mode 100644 index 000000000..1835ccb15 --- /dev/null +++ b/packages/plugins/charts/src/client/Icons.tsx @@ -0,0 +1,99 @@ +import { Icon } from '@nocobase/client'; +import React from 'react'; + +const RadarChart = () => ( + +); +const FunnelChart = () => ( + +); +const ScatterChart = () => ( + +); +const ColumnChart = () => ( + +); + +const BarChart = () => ( + +); + +const LineChart = () => ( + +); + +const PieChart = () => ( + +); + +const AreaChart = () => ( + +); + +Icon.register({ + 'icon-area': (props) => , + 'icon-pie': (props) => , + 'icon-radar': (props) => , + 'icon-funnel': (props) => , + 'icon-line': (props) => , + 'icon-bar': (props) => , + 'icon-column': (props) => , + 'icon-scatter': (props) => , +}); diff --git a/packages/plugins/charts/src/client/chartRenderComponents/index.ts b/packages/plugins/charts/src/client/chartRenderComponents/index.ts new file mode 100644 index 000000000..e2bc7b578 --- /dev/null +++ b/packages/plugins/charts/src/client/chartRenderComponents/index.ts @@ -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; diff --git a/packages/plugins/charts/src/client/hooks/index.ts b/packages/plugins/charts/src/client/hooks/index.ts new file mode 100644 index 000000000..a3ddd1999 --- /dev/null +++ b/packages/plugins/charts/src/client/hooks/index.ts @@ -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 }; diff --git a/packages/plugins/charts/src/client/index.tsx b/packages/plugins/charts/src/client/index.tsx new file mode 100644 index 000000000..816ac1d4f --- /dev/null +++ b/packages/plugins/charts/src/client/index.tsx @@ -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 ( + + + + {props.children} + + + + ); +}); diff --git a/packages/plugins/charts/src/client/locale/en-US.ts b/packages/plugins/charts/src/client/locale/en-US.ts new file mode 100644 index 000000000..056d2b670 --- /dev/null +++ b/packages/plugins/charts/src/client/locale/en-US.ts @@ -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', +}; diff --git a/packages/plugins/charts/src/client/locale/index.ts b/packages/plugins/charts/src/client/locale/index.ts new file mode 100644 index 000000000..71c6c1c68 --- /dev/null +++ b/packages/plugins/charts/src/client/locale/index.ts @@ -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); +} diff --git a/packages/plugins/charts/src/client/locale/ja-JP.ts b/packages/plugins/charts/src/client/locale/ja-JP.ts new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/packages/plugins/charts/src/client/locale/ja-JP.ts @@ -0,0 +1 @@ +export default {}; diff --git a/packages/plugins/charts/src/client/locale/ru-RU.ts b/packages/plugins/charts/src/client/locale/ru-RU.ts new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/packages/plugins/charts/src/client/locale/ru-RU.ts @@ -0,0 +1 @@ +export default {}; diff --git a/packages/plugins/charts/src/client/locale/tr-TR.ts b/packages/plugins/charts/src/client/locale/tr-TR.ts new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/packages/plugins/charts/src/client/locale/tr-TR.ts @@ -0,0 +1 @@ +export default {}; diff --git a/packages/plugins/charts/src/client/locale/zh-CN.ts b/packages/plugins/charts/src/client/locale/zh-CN.ts new file mode 100644 index 000000000..6d689b16f --- /dev/null +++ b/packages/plugins/charts/src/client/locale/zh-CN.ts @@ -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 个「无序名词」字段', +}; diff --git a/packages/plugins/charts/src/client/select/CustomSelect.tsx b/packages/plugins/charts/src/client/select/CustomSelect.tsx new file mode 100644 index 000000000..2348048e0 --- /dev/null +++ b/packages/plugins/charts/src/client/select/CustomSelect.tsx @@ -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 & { 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 ( + { + props.onChange?.(changed === undefined ? null : changed); + }} + mode={mode} + > + + {group1.map((option) => ( + + ))} + + + {group2.map((option) => ( + + ))} + + + ); + }, + mapProps( + { + dataSource: 'options', + loading: true, + }, + (props, field) => { + return { + ...props, + suffixIcon: field?.['loading'] || field?.['validating'] ? : props?.suffixIcon, + }; + }, + ), + mapReadPretty(ReadPretty), +); + +export const CustomSelect = InternalSelect as unknown as typeof InternalSelect & { + ReadPretty: typeof ReadPretty; +}; + +CustomSelect.ReadPretty = ReadPretty; + +export default CustomSelect; diff --git a/packages/plugins/charts/src/client/select/ReadPretty.tsx b/packages/plugins/charts/src/client/select/ReadPretty.tsx new file mode 100644 index 000000000..98cf93408 --- /dev/null +++ b/packages/plugins/charts/src/client/select/ReadPretty.tsx @@ -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; + Object?: React.FC; +}; + +export const ReadPretty = observer((props: any) => { + const fieldNames = { ...defaultFieldNames, ...props.fieldNames }; + const field = useField(); + const compile = useCompile(); + + if (!isValid(props.value)) { + return
; + } + if (isArrayField(field) && field?.value?.length === 0) { + return
; + } + const dataSource = field.dataSource || props.options || []; + const options = getCurrentOptions(field.value, dataSource, fieldNames); + return ( +
+ {options.map((option, key) => ( + + {compile(option[fieldNames.label])} + + ))} +
+ ); +}); diff --git a/packages/plugins/charts/src/client/select/index.md b/packages/plugins/charts/src/client/select/index.md new file mode 100644 index 000000000..e7d0fb21b --- /dev/null +++ b/packages/plugins/charts/src/client/select/index.md @@ -0,0 +1,38 @@ +--- +nav: + path: /client +group: + path: /schema-components +--- + +# Select + +## Examples + +### 单选 + + + +### 多选 + + + +### 值为 Object 类型的 Select + + + +## 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', +}; +``` diff --git a/packages/plugins/charts/src/client/select/index.ts b/packages/plugins/charts/src/client/select/index.ts new file mode 100644 index 000000000..8e772cb69 --- /dev/null +++ b/packages/plugins/charts/src/client/select/index.ts @@ -0,0 +1,2 @@ +export * from './CustomSelect'; +export * from './shared'; diff --git a/packages/plugins/charts/src/client/select/shared.ts b/packages/plugins/charts/src/client/select/shared.ts new file mode 100644 index 000000000..48f54c1ca --- /dev/null +++ b/packages/plugins/charts/src/client/select/shared.ts @@ -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); +}; diff --git a/packages/plugins/charts/src/client/settings/AddNewQuery.tsx b/packages/plugins/charts/src/client/settings/AddNewQuery.tsx new file mode 100644 index 000000000..1dd6d9f5b --- /dev/null +++ b/packages/plugins/charts/src/client/settings/AddNewQuery.tsx @@ -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 = ( + { + setVisible(true); + form.setValues({ type: info.key }); + setSchema(getSchema({ type: info.key }, { form, isNewRecord: true })); + }} + > + JSON + SQL + + API + + Collection + + ); + return ( + + + + + + + ); +}; + +export const EditQuery = () => { + const [visible, setVisible] = useState(false); + const record = useRecord(); + const form = useMemo(() => createForm(), []); + const schema = getSchema(record, { form, isNewRecord: false }); + return ( + + { + form.setValues(record); + setVisible(true); + }} + > + {lang('Edit')} + + + + ); +}; diff --git a/packages/plugins/charts/src/client/settings/ConfigureFields.tsx b/packages/plugins/charts/src/client/settings/ConfigureFields.tsx new file mode 100644 index 000000000..cb15739f6 --- /dev/null +++ b/packages/plugins/charts/src/client/settings/ConfigureFields.tsx @@ -0,0 +1,18 @@ +import { useRecord } from '@nocobase/client'; +import { Table } from 'antd'; +import React from 'react'; + +export const ConfigureFields = () => { + const record = useRecord(); + return ( + + ); +}; diff --git a/packages/plugins/charts/src/client/settings/QueriesTable.tsx b/packages/plugins/charts/src/client/settings/QueriesTable.tsx new file mode 100644 index 000000000..c2cc5f7be --- /dev/null +++ b/packages/plugins/charts/src/client/settings/QueriesTable.tsx @@ -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 ( + + + + ); +}; diff --git a/packages/plugins/charts/src/client/settings/queryTypes.ts b/packages/plugins/charts/src/client/settings/queryTypes.ts new file mode 100644 index 000000000..1bdb06730 --- /dev/null +++ b/packages/plugins/charts/src/client/settings/queryTypes.ts @@ -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]); +}; diff --git a/packages/plugins/charts/src/client/settings/schemas/chartsQueries.ts b/packages/plugins/charts/src/client/settings/schemas/chartsQueries.ts new file mode 100644 index 000000000..5791a80d1 --- /dev/null +++ b/packages/plugins/charts/src/client/settings/schemas/chartsQueries.ts @@ -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 }}', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/packages/plugins/charts/src/client/templates/AreaTemplate.tsx b/packages/plugins/charts/src/client/templates/AreaTemplate.tsx new file mode 100644 index 000000000..018e16cd5 --- /dev/null +++ b/packages/plugins/charts/src/client/templates/AreaTemplate.tsx @@ -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 }, + }, + }, + }, + }, + }, +}; diff --git a/packages/plugins/charts/src/client/templates/BarTemplate.tsx b/packages/plugins/charts/src/client/templates/BarTemplate.tsx new file mode 100644 index 000000000..23cb4cdfe --- /dev/null +++ b/packages/plugins/charts/src/client/templates/BarTemplate.tsx @@ -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 }, + }, + }, + }, + }, + }, +}; diff --git a/packages/plugins/charts/src/client/templates/ColumnTemplate.tsx b/packages/plugins/charts/src/client/templates/ColumnTemplate.tsx new file mode 100644 index 000000000..0614bcdf6 --- /dev/null +++ b/packages/plugins/charts/src/client/templates/ColumnTemplate.tsx @@ -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 }, + }, + }, + }, + }, + }, +}; diff --git a/packages/plugins/charts/src/client/templates/FunnelTemplate.tsx b/packages/plugins/charts/src/client/templates/FunnelTemplate.tsx new file mode 100644 index 000000000..bd7439268 --- /dev/null +++ b/packages/plugins/charts/src/client/templates/FunnelTemplate.tsx @@ -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 }, + }, + }, + }, + }, + }, +}; diff --git a/packages/plugins/charts/src/client/templates/LineTemplate.tsx b/packages/plugins/charts/src/client/templates/LineTemplate.tsx new file mode 100644 index 000000000..bd02df737 --- /dev/null +++ b/packages/plugins/charts/src/client/templates/LineTemplate.tsx @@ -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 }, + }, + }, + }, + }, + }, +}; diff --git a/packages/plugins/charts/src/client/templates/PieTemplate.tsx b/packages/plugins/charts/src/client/templates/PieTemplate.tsx new file mode 100644 index 000000000..8d2753f99 --- /dev/null +++ b/packages/plugins/charts/src/client/templates/PieTemplate.tsx @@ -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 }, + }, + }, + }, + }, + }, +}; diff --git a/packages/plugins/charts/src/client/templates/RadarTemplate.tsx b/packages/plugins/charts/src/client/templates/RadarTemplate.tsx new file mode 100644 index 000000000..770ff0d2a --- /dev/null +++ b/packages/plugins/charts/src/client/templates/RadarTemplate.tsx @@ -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 }, + }, + }, + }, + }, + }, +}; diff --git a/packages/plugins/charts/src/client/templates/ScatterTemplate.tsx b/packages/plugins/charts/src/client/templates/ScatterTemplate.tsx new file mode 100644 index 000000000..78f72e2e6 --- /dev/null +++ b/packages/plugins/charts/src/client/templates/ScatterTemplate.tsx @@ -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 }, + }, + }, + }, + }, + }, +}; diff --git a/packages/plugins/charts/src/client/templates/TableTemplate.tsx b/packages/plugins/charts/src/client/templates/TableTemplate.tsx new file mode 100644 index 000000000..68addb3d9 --- /dev/null +++ b/packages/plugins/charts/src/client/templates/TableTemplate.tsx @@ -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: {}, + }, +}; diff --git a/packages/plugins/charts/src/client/templates/index.ts b/packages/plugins/charts/src/client/templates/index.ts new file mode 100644 index 000000000..49171d4db --- /dev/null +++ b/packages/plugins/charts/src/client/templates/index.ts @@ -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); diff --git a/packages/plugins/charts/src/client/utils.ts b/packages/plugins/charts/src/client/utils.ts new file mode 100644 index 000000000..3bf0a3eef --- /dev/null +++ b/packages/plugins/charts/src/client/utils.ts @@ -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 }; diff --git a/packages/plugins/charts/src/index.ts b/packages/plugins/charts/src/index.ts new file mode 100644 index 000000000..7ddad5814 --- /dev/null +++ b/packages/plugins/charts/src/index.ts @@ -0,0 +1 @@ +export { default } from './server'; diff --git a/packages/plugins/charts/src/server/actions/chartsQueries.ts b/packages/plugins/charts/src/server/actions/chartsQueries.ts new file mode 100644 index 000000000..788fdddcb --- /dev/null +++ b/packages/plugins/charts/src/server/actions/chartsQueries.ts @@ -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(); +}; diff --git a/packages/plugins/charts/src/server/actions/index.ts b/packages/plugins/charts/src/server/actions/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/plugins/charts/src/server/collections/.gitkeep b/packages/plugins/charts/src/server/collections/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/plugins/charts/src/server/collections/chartsQueries.ts b/packages/plugins/charts/src/server/collections/chartsQueries.ts new file mode 100644 index 000000000..1f6b02ce9 --- /dev/null +++ b/packages/plugins/charts/src/server/collections/chartsQueries.ts @@ -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: [], + }, + ], +}); diff --git a/packages/plugins/charts/src/server/index.ts b/packages/plugins/charts/src/server/index.ts new file mode 100644 index 000000000..b68aea57f --- /dev/null +++ b/packages/plugins/charts/src/server/index.ts @@ -0,0 +1 @@ +export { default } from './plugin'; diff --git a/packages/plugins/charts/src/server/plugin.ts b/packages/plugins/charts/src/server/plugin.ts new file mode 100644 index 000000000..0e1cdc160 --- /dev/null +++ b/packages/plugins/charts/src/server/plugin.ts @@ -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; diff --git a/packages/plugins/charts/src/server/query.ts b/packages/plugins/charts/src/server/query.ts new file mode 100644 index 000000000..0802b1a3f --- /dev/null +++ b/packages/plugins/charts/src/server/query.ts @@ -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; diff --git a/packages/plugins/charts/src/server/shared/index.ts b/packages/plugins/charts/src/server/shared/index.ts new file mode 100644 index 000000000..3fa6adc08 --- /dev/null +++ b/packages/plugins/charts/src/server/shared/index.ts @@ -0,0 +1,2 @@ +const choicesTypeInterfaceArray = ['radioGroup', 'select']; +export { choicesTypeInterfaceArray }; diff --git a/packages/presets/nocobase/package.json b/packages/presets/nocobase/package.json index e46002d5e..4ebb08b2d 100644 --- a/packages/presets/nocobase/package.json +++ b/packages/presets/nocobase/package.json @@ -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": { diff --git a/packages/presets/nocobase/src/index.ts b/packages/presets/nocobase/src/index.ts index 50a85ed9a..12e33e46e 100644 --- a/packages/presets/nocobase/src/index.ts +++ b/packages/presets/nocobase/src/index.ts @@ -23,6 +23,7 @@ export class PresetNocoBase extends Plugin { 'duplicator', 'iframe-block', 'formula-field', + 'charts', ]; localPlugins = [ diff --git a/packages/samples/hello/src/client/index.tsx b/packages/samples/hello/src/client/index.tsx index 7df392ca6..165c259ae 100644 --- a/packages/samples/hello/src/client/index.tsx +++ b/packages/samples/hello/src/client/index.tsx @@ -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', diff --git a/yarn.lock b/yarn.lock index 947ba8486..c07e0c32e 100644 --- a/yarn.lock +++ b/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"