diff --git a/packages/plugins/data-visualization/src/client/block/ChartBlock.tsx b/packages/plugins/data-visualization/src/client/block/ChartBlock.tsx index 08487aa44..7a1d78e26 100644 --- a/packages/plugins/data-visualization/src/client/block/ChartBlock.tsx +++ b/packages/plugins/data-visualization/src/client/block/ChartBlock.tsx @@ -1,21 +1,23 @@ import { SchemaInitializerButtonContext, useDesignable } from '@nocobase/client'; import React, { useState } from 'react'; +import { ChartRendererProvider } from '../renderer'; import { ChartConfigContext, ChartConfigCurrent, ChartConfigure } from './ChartConfigure'; export const ChartV2Block: React.FC = (props) => { const { insertAdjacent } = useDesignable(); const [visible, setVisible] = useState(false); const [current, setCurrent] = useState({} as any); - const [data, setData] = useState([]); const [initialVisible, setInitialVisible] = useState(false); const [searchValue, setSearchValue] = useState(''); return ( - + {props.children} - insertAdjacent('beforeEnd', schema, options)} /> + + insertAdjacent('beforeEnd', schema, options)} /> + ); diff --git a/packages/plugins/data-visualization/src/client/block/ChartBlockInitializer.tsx b/packages/plugins/data-visualization/src/client/block/ChartBlockInitializer.tsx index 80a64e111..6df4d1104 100644 --- a/packages/plugins/data-visualization/src/client/block/ChartBlockInitializer.tsx +++ b/packages/plugins/data-visualization/src/client/block/ChartBlockInitializer.tsx @@ -8,13 +8,12 @@ import { ChartConfigContext } from './ChartConfigure'; const itemWrap = SchemaInitializer.itemWrap; const ConfigureButton = itemWrap((props) => { - const { setVisible, setCurrent, setData } = useContext(ChartConfigContext); + const { setVisible, setCurrent } = useContext(ChartConfigContext); return ( { - setCurrent({ schema: {}, field: null, collection: props.item?.name }); - setData([]); + setCurrent({ schema: {}, field: null, collection: props.item?.name, service: null, data: undefined }); setVisible(true); }} /> diff --git a/packages/plugins/data-visualization/src/client/block/ChartConfigure.tsx b/packages/plugins/data-visualization/src/client/block/ChartConfigure.tsx index 50a592cf1..a69144e72 100644 --- a/packages/plugins/data-visualization/src/client/block/ChartConfigure.tsx +++ b/packages/plugins/data-visualization/src/client/block/ChartConfigure.tsx @@ -20,7 +20,6 @@ import { import { Alert, App, Button, Card, Col, Modal, Row, Space, Table, Tabs, Typography } from 'antd'; import { cloneDeep, isEqual } from 'lodash'; import React, { createContext, useContext, useMemo, useRef } from 'react'; -import { useTranslation } from 'react-i18next'; import { useChartFields, useCollectionOptions, @@ -32,8 +31,8 @@ import { useTransformers, } from '../hooks'; import { useChartsTranslation } from '../locale'; -import { ChartRenderer, ChartRendererProvider, useChartTypes, useCharts } from '../renderer'; -import { createRendererSchema, getField, getSelectedFields } from '../utils'; +import { ChartRenderer, ChartRendererContext, useChartTypes, useCharts, useDefaultChartType } from '../renderer'; +import { createRendererSchema, getField, getSelectedFields, processData } from '../utils'; import { getConfigSchema, querySchema, transformSchema } from './schemas/configure'; const { Paragraph, Text } = Typography; @@ -41,6 +40,9 @@ export type ChartConfigCurrent = { schema: ISchema; field: any; collection: string; + service: any; + initialValues?: any; + data: any[]; }; export type SelectedField = { @@ -53,8 +55,6 @@ export const ChartConfigContext = createContext<{ setVisible?: (visible: boolean) => void; current?: ChartConfigCurrent; setCurrent?: (current: ChartConfigCurrent) => void; - data?: any[] | string; - setData?: (data: any[] | string) => void; }>({ visible: true, }); @@ -68,17 +68,16 @@ export const ChartConfigure: React.FC<{ }, ) => void; }> & { - Renderer: React.FC<{ - runQuery?: any; - }>; + Renderer: React.FC; Config: React.FC; Query: React.FC; Transform: React.FC; Data: React.FC; } = (props) => { const { t } = useChartsTranslation(); + const { service } = useContext(ChartRendererContext); const { visible, setVisible, current } = useContext(ChartConfigContext); - const { schema, field, collection } = current || {}; + const { schema, field, collection, initialValues } = current || {}; const { dn } = useDesignable(); const { modal } = App.useApp(); const { insert } = props; @@ -125,53 +124,48 @@ export const ChartConfigure: React.FC<{ setMeasures(cloneDeep(currentMeasures)); setDimensions(cloneDeep(currentDimensions)); }; - const chartTypes = useChartTypes(); + const chartType = useDefaultChartType(); const form = useMemo( () => { - const chartType = chartTypes[0]?.children?.[0]?.value; return createForm({ - values: { config: { chartType }, ...schema?.['x-decorator-props'], collection, data: '' }, + values: { config: { chartType }, ...(initialValues || field?.decoratorProps), collection }, effects: (form) => { onFieldChange('config.chartType', () => initChart(true)); - onFormInit(() => queryReact(form)); + onFormInit(() => { + queryReact(form); + }); }, }); }, // visible, collection added here to re-initialize form when visible, collection change // eslint-disable-next-line react-hooks/exhaustive-deps - [schema, visible, collection], + [field, visible, collection], ); - const runQuery = useRef(null); - const RunButton: React.FC = () => { - const [loading, setLoading] = React.useState(false); - return ( - - ); - }; + try { + await service.runAsync(collection, form.values.query); + } catch (e) { + console.log(e); + } + queryReact(form, initChart); + }} + > + {t('Run query')} + + ); const queryRef = useRef(null); const configRef = useRef(null); @@ -181,6 +175,13 @@ export const ChartConfigure: React.FC<{ open={visible} onOk={() => { const { query, config, transform, mode } = form.values; + const afterSave = () => { + setVisible(false); + current.service?.run(collection, query); + queryRef.current.scrollTop = 0; + configRef.current.scrollTop = 0; + service.mutate(undefined); + }; const rendererProps = { query, config, @@ -195,13 +196,11 @@ export const ChartConfigure: React.FC<{ dn.emit('patch', { schema, }); - setVisible(false); - queryRef.current.scrollTop = 0; - configRef.current.scrollTop = 0; + afterSave(); return; } insert(createRendererSchema(rendererProps), { - onSuccess: () => setVisible(false), + onSuccess: afterSave, wrap: gridRowColWrap, }); }} @@ -216,6 +215,7 @@ export const ChartConfigure: React.FC<{ setVisible(false); queryRef.current.scrollTop = 0; configRef.current.scrollTop = 0; + service.mutate(undefined); }, }); }} @@ -231,6 +231,7 @@ export const ChartConfigure: React.FC<{ style={{ height: 'calc(100vh - 300px)', overflow: 'auto', + margin: '12px 0 12px 12px', }} ref={queryRef} > @@ -256,6 +257,7 @@ export const ChartConfigure: React.FC<{ style={{ height: 'calc(100vh - 300px)', overflow: 'auto', + margin: '12px 3px 12px 3px', }} ref={configRef} > @@ -276,8 +278,12 @@ export const ChartConfigure: React.FC<{ - - + + @@ -288,7 +294,8 @@ export const ChartConfigure: React.FC<{ ChartConfigure.Renderer = function Renderer(props) { const { current } = useContext(ChartConfigContext); - const { collection } = current || {}; + const { collection, data } = current || {}; + const { service } = useContext(ChartRendererContext); return ( {(form) => { @@ -297,15 +304,9 @@ ChartConfigure.Renderer = function Renderer(props) { const config = cloneDeep(form.values.config); const transform = cloneDeep(form.values.transform); return ( - - - + + + ); }} @@ -314,27 +315,30 @@ ChartConfigure.Renderer = function Renderer(props) { ChartConfigure.Query = function Query() { const { t } = useChartsTranslation(); - const { t: lang } = useTranslation(); const fields = useFieldsWithAssociation(); const useFormatterOptions = useFormatters(fields); const collectionOptions = useCollectionOptions(); const { current, setCurrent } = useContext(ChartConfigContext); const { collection } = current || {}; const fieldOptions = useCollectionFieldsOptions(collection, 1); - const compiledFieldOptions = Schema.compile(fieldOptions, { t: lang }); + const compiledFieldOptions = Schema.compile(fieldOptions, { t }); const filterOptions = useCollectionFilterOptions(collection); - const formCollapse = FormCollapse.createFormCollapse(['measures', 'dimensions', 'filter', 'sort']); + + const { service } = useContext(ChartRendererContext); const onCollectionChange = (value: string) => { const { schema, field } = current; - const newSchema = { ...schema }; - newSchema['x-decorator-props'] = { collection: value }; - newSchema['x-acl-action'] = `${value}:list`; setCurrent({ - schema: newSchema, + schema, field, collection: value, + service: current.service, + initialValues: {}, + data: undefined, }); + service.mutate(undefined); }; + + const formCollapse = FormCollapse.createFormCollapse(['measures', 'dimensions', 'filter', 'sort']); const FromSql = () => ( From {current?.collection} @@ -430,22 +434,38 @@ ChartConfigure.Transform = function Transform() { }; ChartConfigure.Data = function Data() { - const { data } = useContext(ChartConfigContext); + const { t } = useChartsTranslation(); + const { current } = useContext(ChartConfigContext); + const { service } = useContext(ChartRendererContext); const fields = useFieldsWithAssociation(); - return Array.isArray(data) ? ( - { - const field = getField(fields, col.split('.')); - return { - title: field?.label || col, - dataIndex: col, - key: col, - }; - })} - size="small" - /> + const data = processData(fields, service?.data || current?.data || [], { t }); + const error = service?.error; + return !error ? ( +
+
{ + const field = getField(fields, col.split('.')); + return { + title: field?.label || col, + dataIndex: col, + key: col, + }; + })} + size="small" + /> + ) : ( - + error.message).join('\n') || error.message} + showIcon + /> ); }; diff --git a/packages/plugins/data-visualization/src/client/renderer/ChartLibrary.tsx b/packages/plugins/data-visualization/src/client/renderer/ChartLibrary.tsx index 54a9cf844..fd625ed79 100644 --- a/packages/plugins/data-visualization/src/client/renderer/ChartLibrary.tsx +++ b/packages/plugins/data-visualization/src/client/renderer/ChartLibrary.tsx @@ -90,6 +90,11 @@ export const useChartTypes = (): { }, []); }; +export const useDefaultChartType = () => { + const chartTypes = useChartTypes(); + return chartTypes[0]?.children?.[0]?.value; +}; + export const useToggleChartLibrary = () => { const ctx = useContext(ChartLibraryContext); return { diff --git a/packages/plugins/data-visualization/src/client/renderer/ChartRenderer.tsx b/packages/plugins/data-visualization/src/client/renderer/ChartRenderer.tsx index 72a1923bf..da781d5c9 100644 --- a/packages/plugins/data-visualization/src/client/renderer/ChartRenderer.tsx +++ b/packages/plugins/data-visualization/src/client/renderer/ChartRenderer.tsx @@ -1,107 +1,34 @@ import { useField, useFieldSchema } from '@formily/react'; import { GeneralSchemaDesigner, - gridRowColWrap, SchemaSettings, + gridRowColWrap, useAPIClient, useCollection, useDesignable, - useRequest, } from '@nocobase/client'; -import { Empty, Result, Typography } from 'antd'; -import React, { useContext, useEffect, useState } from 'react'; +import { Empty, Result, Spin, Typography } from 'antd'; +import React, { useContext } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { ChartConfigContext } from '../block'; -import { useFieldsWithAssociation, useFieldTransformer } from '../hooks'; +import { useFieldTransformer, useFieldsWithAssociation } from '../hooks'; import { useChartsTranslation } from '../locale'; -import { createRendererSchema, getField, parseField, processData } from '../utils'; +import { createRendererSchema, getField, processData } from '../utils'; import { useCharts } from './ChartLibrary'; -import { ChartRendererContext, DimensionProps, MeasureProps, QueryProps } from './ChartRendererProvider'; +import { ChartRendererContext } from './ChartRendererProvider'; const { Paragraph, Text } = Typography; -export const ChartRenderer: React.FC<{ - configuring?: boolean; - runQuery?: any; -}> & { +export const ChartRenderer: React.FC & { Designer: React.FC; } = (props) => { const { t } = useChartsTranslation(); - const { setData: setQueryData, current } = useContext(ChartConfigContext); - const { query, config, collection, transform } = useContext(ChartRendererContext); - const { configuring, runQuery } = props; + const ctx = useContext(ChartRendererContext); + const { config, transform, collection, service, data: _data } = ctx; + const fields = useFieldsWithAssociation(collection); + const data = processData(fields, service?.data || _data || [], { t }); const general = config?.general || {}; const advanced = config?.advanced || {}; - const schema = useFieldSchema(); - const currentSchema = schema || current?.schema; - const fields = useFieldsWithAssociation(collection); const api = useAPIClient(); - const [data, setData] = useState([]); - const { runAsync } = useRequest( - (query) => - api - .request({ - url: 'charts:query', - method: 'POST', - data: { - uid: currentSchema?.['x-uid'], - collection, - ...query, - dimensions: (query?.dimensions || []).map((item: DimensionProps) => { - const dimension = { ...item }; - if (item.format && !item.alias) { - const { alias } = parseField(item.field); - dimension.alias = alias; - } - return dimension; - }), - measures: (query?.measures || []).map((item: MeasureProps) => { - const measure = { ...item }; - if (item.aggregation && !item.alias) { - const { alias } = parseField(item.field); - measure.alias = alias; - } - return measure; - }), - }, - }) - .then((res) => { - const data = res?.data?.data || []; - return processData(fields, data, { t }); - }), - { - manual: true, - onSuccess: (data) => { - setData(data); - }, - onFinally(params, data, error: any) { - if (!configuring) { - return; - } - if (error) { - const message = error?.response?.data?.errors?.map?.((error: any) => error.message).join('\n'); - setQueryData(message || error.message); - return; - } - setQueryData(data); - }, - }, - ); - - useEffect(() => { - setData([]); - const run = async (query: QueryProps) => { - if ( - query?.measures?.length - // || (query?.sql?.fields && query?.sql?.clauses) - ) { - await runAsync(query); - } - }; - if (runQuery) { - runQuery.current = run; - } - run(query); - }, [query, runAsync, runQuery]); const charts = useCharts(); const chart = charts[config?.chartType]; @@ -137,16 +64,21 @@ export const ChartRenderer: React.FC<{ ); - return data && data.length ? ( - - ) : ( - - ); + if (service.loading) { + return ; + } + + if (!(data && data.length)) { + return ; + } + + return ; }; ChartRenderer.Designer = function Designer() { const { t } = useChartsTranslation(); const { setVisible, setCurrent } = useContext(ChartConfigContext); + const { service } = useContext(ChartRendererContext); const field = useField(); const schema = useFieldSchema(); const { insertAdjacent } = useDesignable(); @@ -156,7 +88,7 @@ ChartRenderer.Designer = function Designer() { { - setCurrent({ schema, field, collection: name }); + setCurrent({ schema, field, collection: name, service, data: service?.data }); setVisible(true); }} > diff --git a/packages/plugins/data-visualization/src/client/renderer/ChartRendererProvider.tsx b/packages/plugins/data-visualization/src/client/renderer/ChartRendererProvider.tsx index 508b968d0..d1c925f15 100644 --- a/packages/plugins/data-visualization/src/client/renderer/ChartRendererProvider.tsx +++ b/packages/plugins/data-visualization/src/client/renderer/ChartRendererProvider.tsx @@ -1,6 +1,8 @@ import { css } from '@emotion/css'; -import { MaybeCollectionProvider } from '@nocobase/client'; +import { useFieldSchema } from '@formily/react'; +import { MaybeCollectionProvider, useAPIClient, useRequest } from '@nocobase/client'; import React, { createContext } from 'react'; +import { parseField } from '../utils'; export type MeasureProps = { field: string | string[]; @@ -47,10 +49,62 @@ export type ChartRendererProps = { mode?: 'builder' | 'sql'; }; -export const ChartRendererContext = createContext({} as any); +export const ChartRendererContext = createContext<{ + collection: string; + config?: { + chartType: string; + general: any; + advanced: any; + }; + transform?: TransformProps[]; + service: any; + data?: any[]; +}>({} as any); export const ChartRendererProvider: React.FC = (props) => { - const { collection } = props; + const { query, config, collection, transform } = props; + const schema = useFieldSchema(); + const api = useAPIClient(); + const service = useRequest( + (collection, query) => + new Promise((resolve, reject) => { + if (!(collection && query?.measures?.length)) return resolve(undefined); + api + .request({ + url: 'charts:query', + method: 'POST', + data: { + uid: schema?.['x-uid'], + collection, + ...query, + dimensions: (query?.dimensions || []).map((item: DimensionProps) => { + const dimension = { ...item }; + if (item.format && !item.alias) { + const { alias } = parseField(item.field); + dimension.alias = alias; + } + return dimension; + }), + measures: (query?.measures || []).map((item: MeasureProps) => { + const measure = { ...item }; + if (item.aggregation && !item.alias) { + const { alias } = parseField(item.field); + measure.alias = alias; + } + return measure; + }), + }, + }) + .then((res) => { + resolve(res?.data?.data); + }) + .catch(reject); + }), + { + defaultParams: [collection, query], + }, + ); + return (
= (props) => { .ant-card { box-shadow: none; } - .ant-card-body { - padding: 0; - } `} > - {props.children} + + {props.children} +
); diff --git a/packages/plugins/data-visualization/src/client/renderer/library/G2PlotLibrary.tsx b/packages/plugins/data-visualization/src/client/renderer/library/G2PlotLibrary.tsx index 28ceab0b1..6436c462a 100644 --- a/packages/plugins/data-visualization/src/client/renderer/library/G2PlotLibrary.tsx +++ b/packages/plugins/data-visualization/src/client/renderer/library/G2PlotLibrary.tsx @@ -181,7 +181,7 @@ export const G2PlotLibrary: Charts = { 'x-reactions': '{{ useChartFields }}', 'x-component-props': { style: { - 'min-width': '200px', + minWidth: '200px', }, }, required: true,