fix(bi): eliminate redundancy queries (#2268)

* fix(bi): eliminate redundancy queries

* fix: reset data after close configure pane

* fix: enum transform bug

* fix: reset form after clicking cancel button

* fix: reset form when changing collection
This commit is contained in:
YANG QIA 2023-07-19 16:34:52 +08:00 committed by GitHub
parent 2d8451f2dd
commit d80213be5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 196 additions and 185 deletions

View File

@ -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<ChartConfigCurrent>({} as any);
const [data, setData] = useState<string | any[]>([]);
const [initialVisible, setInitialVisible] = useState(false);
const [searchValue, setSearchValue] = useState('');
return (
<SchemaInitializerButtonContext.Provider
value={{ visible: initialVisible, setVisible: setInitialVisible, searchValue, setSearchValue }}
>
<ChartConfigContext.Provider value={{ visible, setVisible, current, setCurrent, data, setData }}>
<ChartConfigContext.Provider value={{ visible, setVisible, current, setCurrent }}>
{props.children}
<ChartConfigure insert={(schema, options) => insertAdjacent('beforeEnd', schema, options)} />
<ChartRendererProvider {...current.field?.decoratorProps}>
<ChartConfigure insert={(schema, options) => insertAdjacent('beforeEnd', schema, options)} />
</ChartRendererProvider>
</ChartConfigContext.Provider>
</SchemaInitializerButtonContext.Provider>
);

View File

@ -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 (
<SchemaInitializer.Item
{...props}
onClick={() => {
setCurrent({ schema: {}, field: null, collection: props.item?.name });
setData([]);
setCurrent({ schema: {}, field: null, collection: props.item?.name, service: null, data: undefined });
setVisible(true);
}}
/>

View File

@ -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 (
<Button
type="link"
loading={loading}
icon={<RightSquareOutlined />}
onClick={async () => {
const queryField = form.query('query').take() as ObjectField;
try {
await queryField?.validate();
} catch (e) {
return;
}
const RunButton: React.FC = () => (
<Button
type="link"
loading={service?.loading}
icon={<RightSquareOutlined />}
onClick={async () => {
const queryField = form.query('query').take() as ObjectField;
try {
await queryField?.validate();
} catch (e) {
return;
}
setLoading(true);
try {
await runQuery?.current(form.values.query);
} catch (e) {
console.log(e);
}
queryReact(form, initChart);
setLoading(false);
}}
>
{t('Run query')}
</Button>
);
};
try {
await service.runAsync(collection, form.values.query);
} catch (e) {
console.log(e);
}
queryReact(form, initChart);
}}
>
{t('Run query')}
</Button>
);
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<{
</Card>
</Col>
<Col span={11}>
<Card>
<ChartConfigure.Renderer runQuery={runQuery} />
<Card
style={{
margin: '12px 12px 12px 0',
}}
>
<ChartConfigure.Renderer />
</Card>
</Col>
</Row>
@ -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 (
<FormConsumer>
{(form) => {
@ -297,15 +304,9 @@ ChartConfigure.Renderer = function Renderer(props) {
const config = cloneDeep(form.values.config);
const transform = cloneDeep(form.values.transform);
return (
<ChartRendererProvider
collection={collection}
query={form.values.query}
config={config}
transform={transform}
mode={form.values.mode}
>
<ChartRenderer configuring={true} {...props} />
</ChartRendererProvider>
<ChartRendererContext.Provider value={{ collection, config, transform, service, data }}>
<ChartRenderer {...props} />
</ChartRendererContext.Provider>
);
}}
</FormConsumer>
@ -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 = () => (
<Text code>
From <span style={{ color: '#1890ff' }}>{current?.collection}</span>
@ -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) ? (
<Table
dataSource={data}
columns={Object.keys(data[0] || {}).map((col) => {
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 ? (
<div
style={{
overflowX: 'auto',
overflowY: 'hidden',
}}
>
<Table
dataSource={data}
columns={Object.keys(data[0] || {}).map((col) => {
const field = getField(fields, col.split('.'));
return {
title: field?.label || col,
dataIndex: col,
key: col,
};
})}
size="small"
/>
</div>
) : (
<Alert message="Error" type="error" description={data} showIcon />
<Alert
message="Error"
type="error"
description={error?.response?.data?.errors?.map?.((error: any) => error.message).join('\n') || error.message}
showIcon
/>
);
};

View File

@ -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 {

View File

@ -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<any[]>([]);
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<{
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('Please configure chart')} />
);
return data && data.length ? (
<C />
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('Please configure and run query')} />
);
if (service.loading) {
return <Spin />;
}
if (!(data && data.length)) {
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('Please configure and run query')} />;
}
return <C />;
};
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() {
<SchemaSettings.Item
key="configure"
onClick={() => {
setCurrent({ schema, field, collection: name });
setCurrent({ schema, field, collection: name, service, data: service?.data });
setVisible(true);
}}
>

View File

@ -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<ChartRendererProps>({} 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<ChartRendererProps> = (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 (
<MaybeCollectionProvider collection={collection}>
<div
@ -58,12 +112,11 @@ export const ChartRendererProvider: React.FC<ChartRendererProps> = (props) => {
.ant-card {
box-shadow: none;
}
.ant-card-body {
padding: 0;
}
`}
>
<ChartRendererContext.Provider value={{ ...props }}>{props.children}</ChartRendererContext.Provider>
<ChartRendererContext.Provider value={{ collection, config, transform, service }}>
{props.children}
</ChartRendererContext.Provider>
</div>
</MaybeCollectionProvider>
);

View File

@ -181,7 +181,7 @@ export const G2PlotLibrary: Charts = {
'x-reactions': '{{ useChartFields }}',
'x-component-props': {
style: {
'min-width': '200px',
minWidth: '200px',
},
},
required: true,