tachybase_todo/packages/plugins/@nocobase/plugin-data-visualization/src/client/configure/ChartConfigure.tsx
sealday 2d991ba7cf 合并对main的所有修改
<!-- Note -->
<!-- This is a template for submitting a new feature.
Use the bug fix template if you're submitting a bug fix pull request by adding `template=bug_fix.md` to your pull request URL. -->

<!-- Describe the new feature or modification to an existing feature clearly and consciously. -->

<!-- Explain the reason for adding or modifying this feature. -->

<!-- Provide a technically detailed description of the key changes made. -->
- Frontend
- Backend

<!-- Provide any suggestions or recommendations for improvements in the testing plan. -->

<!-- Identify any potential risks or issues that may arise from the new feature or modification. -->

<!-- Including any screenshots of the new feature or modification. -->

Co-authored-by: sealday <sealday@gmail.com>
Co-authored-by: wjh <wwwjh0710@163.com>
Co-authored-by: 吕延祥 <2256334253@qq.com>
Reviewed-on: daoyoucloud/nocobase#317
2024-03-10 04:06:04 +08:00

440 lines
13 KiB
TypeScript

import { RightSquareOutlined } from '@ant-design/icons';
import { ArrayItems, Editable, FormCollapse, FormItem, FormLayout, Switch } from '@formily/antd-v5';
import { Form as FormType, ObjectField, createForm, onFieldChange, onFormInit } from '@formily/core';
import { FormConsumer, ISchema, Schema } from '@formily/react';
import {
AutoComplete,
FormProvider,
SchemaComponent,
gridRowColWrap,
useCollectionFieldsOptions,
useCollectionFilterOptions,
useDesignable,
} from '@nocobase/client';
import { Alert, App, Button, Card, Col, Modal, Row, Space, Table, Tabs, Typography } from 'antd';
import { cloneDeep, isEqual } from 'lodash';
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
import {
useChartFields,
useCollectionOptions,
useData,
useFieldTypes,
useFieldsWithAssociation,
useFormatters,
useOrderFieldsOptions,
useOrderReaction,
useTransformers,
useTransformersDecimal,
} from '../hooks';
import { useChartsTranslation } from '../locale';
import { ChartRenderer, ChartRendererContext } from '../renderer';
import { createRendererSchema, getField, getSelectedFields } from '../utils';
import { getConfigSchema, querySchema, transformSchema } from './schemas/configure';
import { useChartTypes, useCharts, useDefaultChartType } from '../chart/group';
import { FilterDynamicComponent } from './FilterDynamicComponent';
import { ChartConfigContext } from './ChartConfigProvider';
const { Paragraph, Text } = Typography;
export type SelectedField = {
field: string | string[];
alias?: string;
};
export const ChartConfigure: React.FC<{
insert: (
s: ISchema,
options: {
onSuccess: () => void;
wrap?: (schema: ISchema) => ISchema;
},
) => void;
}> & {
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, initialValues } = current || {};
const { dn } = useDesignable();
const { modal } = App.useApp();
const { insert } = props;
const charts = useCharts();
const fields = useFieldsWithAssociation(collection);
const initChart = (overwrite = false) => {
if (!form.modified) {
return;
}
const chartType = form.values.config?.chartType;
if (!chartType) {
return;
}
const chart = charts[chartType];
const init = chart?.init;
if (!init) {
if (overwrite) {
form.values.config.general = {};
form.values.config.advanced = {};
}
return;
}
const query = form.values.query;
const selectedFields = getSelectedFields(fields, query);
const { general, advanced } = chart.init(selectedFields, query);
if (general || overwrite) {
form.values.config.general = general;
}
if (advanced || overwrite) {
form.values.config.advanced = advanced || {};
}
};
const [measures, setMeasures] = React.useState([]);
const [dimensions, setDimensions] = React.useState([]);
const queryReact = (form: FormType, reaction?: () => void) => {
const currentMeasures = form.values.query?.measures.filter((item) => item.field) || [];
const currentDimensions = form.values.query?.dimensions.filter((item) => item.field) || [];
if (isEqual(currentMeasures, measures) && isEqual(currentDimensions, dimensions)) {
return;
}
reaction?.();
setMeasures(cloneDeep(currentMeasures));
setDimensions(cloneDeep(currentDimensions));
};
const chartType = useDefaultChartType();
const form = useMemo(
() =>
createForm({
values: { config: { chartType }, ...(initialValues || field?.decoratorProps), collection },
effects: (form) => {
onFieldChange('config.chartType', () => initChart(true));
onFormInit(() => queryReact(form));
},
}),
// visible, collection added here to re-initialize form when visible, collection change
// eslint-disable-next-line react-hooks/exhaustive-deps
[field, visible, collection],
);
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;
}
try {
await service.runAsync(collection, form.values.query, true);
} catch (e) {
console.log(e);
}
queryReact(form, initChart);
}}
>
{t('Run query')}
</Button>
);
const queryRef = useRef(null);
const configRef = useRef(null);
useEffect(() => {
if (!visible) {
return;
}
service.run(collection, field?.decoratorProps?.query, 'configure');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible]);
return (
<Modal
title={t('Configure chart')}
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,
collection,
transform,
mode: mode || 'builder',
};
if (schema && schema['x-uid']) {
schema['x-decorator-props'] = rendererProps;
field.decoratorProps = rendererProps;
field['x-acl-action'] = `${collection}:list`;
dn.emit('patch', {
schema,
});
afterSave();
return;
}
insert(gridRowColWrap(createRendererSchema(rendererProps)), {
onSuccess: afterSave,
});
}}
onCancel={() => {
modal.confirm({
title: t('Are you sure to cancel?'),
content: t('You changes are not saved. If you click OK, your changes will be lost.'),
okButtonProps: {
danger: true,
},
onOk: () => {
setVisible(false);
queryRef.current.scrollTop = 0;
configRef.current.scrollTop = 0;
service.mutate(undefined);
},
});
}}
width={'95%'}
styles={{
body: {
background: 'rgba(128, 128, 128, 0.08)',
},
}}
>
<FormProvider form={form}>
<FormLayout layout="vertical">
<Row gutter={8}>
<Col span={7}>
<Card
style={{
height: 'calc(100vh - 300px)',
overflow: 'auto',
margin: '12px 0 12px 12px',
}}
ref={queryRef}
>
<Tabs
tabBarExtraContent={<RunButton />}
items={[
{
label: t('Query'),
key: 'query',
children: <ChartConfigure.Query />,
},
{
label: t('Data'),
key: 'data',
children: <ChartConfigure.Data />,
},
]}
/>
</Card>
</Col>
<Col span={6}>
<Card
style={{
height: 'calc(100vh - 300px)',
overflow: 'auto',
margin: '12px 3px 12px 3px',
}}
ref={configRef}
>
<Tabs
items={[
{
label: t('Chart'),
key: 'chart',
children: <ChartConfigure.Config />,
},
{
label: t('Transform'),
key: 'transform',
children: <ChartConfigure.Transform />,
},
]}
/>
</Card>
</Col>
<Col span={11}>
<Card
style={{
margin: '12px 12px 12px 0',
}}
>
<ChartConfigure.Renderer />
</Card>
</Col>
</Row>
</FormLayout>
</FormProvider>
</Modal>
);
};
ChartConfigure.Renderer = function Renderer(props) {
const { current } = useContext(ChartConfigContext);
const { collection, data } = current || {};
const { service } = useContext(ChartRendererContext);
return (
<FormConsumer>
{(form) => {
// Any change of config and transform will trigger rerender
// Change of query only trigger rerender when "Run query" button is clicked
const config = cloneDeep(form.values.config);
const transform = cloneDeep(form.values.transform);
return (
<ChartRendererContext.Provider value={{ collection, config, transform, service, data }}>
<ChartRenderer {...props} />
</ChartRendererContext.Provider>
);
}}
</FormConsumer>
);
};
ChartConfigure.Query = function Query() {
const { t } = useChartsTranslation();
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 });
const filterOptions = useCollectionFilterOptions(collection);
const { service } = useContext(ChartRendererContext);
const onCollectionChange = (value: string) => {
const { schema, field } = current;
setCurrent({
schema,
field,
collection: value,
dataSource: current.dataSource,
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>
</Text>
);
return (
<SchemaComponent
schema={querySchema}
scope={{
t,
formCollapse,
fieldOptions: compiledFieldOptions,
filterOptions,
useOrderOptions: useOrderFieldsOptions(compiledFieldOptions, fields),
collectionOptions,
useFormatterOptions,
onCollectionChange,
collection: current?.collection,
useOrderReaction: useOrderReaction(compiledFieldOptions, fields),
}}
components={{ ArrayItems, Editable, FormCollapse, FormItem, Space, Switch, FromSql, FilterDynamicComponent }}
/>
);
};
ChartConfigure.Config = function Config() {
const { t } = useChartsTranslation();
const chartTypes = useChartTypes();
const fields = useFieldsWithAssociation();
const charts = useCharts();
const getChartFields = useChartFields(fields);
const getReference = (chartType: string) => {
const reference = charts[chartType]?.getReference?.();
if (!reference) return '';
const { title, link } = reference;
return (
<span>
{t('Config reference: ')}
<a href={link} target="_blank" rel="noreferrer">
{t(title)}
</a>
</span>
);
};
return (
<FormConsumer>
{(form) => {
const chartType = form.values.config?.chartType;
const chart = charts[chartType];
const schema = chart?.schema || {};
return (
<SchemaComponent
schema={getConfigSchema(schema)}
scope={{ t, chartTypes, useChartFields: getChartFields, getReference }}
components={{ FormItem, ArrayItems, Space, AutoComplete }}
/>
);
}}
</FormConsumer>
);
};
ChartConfigure.Transform = function Transform() {
const { t } = useChartsTranslation();
const fields = useFieldsWithAssociation();
const useFieldTypeOptions = useFieldTypes(fields);
const getChartFields = useChartFields(fields);
return (
<SchemaComponent
schema={transformSchema}
components={{ FormItem, ArrayItems, Space }}
scope={{ useChartFields: getChartFields, useFieldTypeOptions, useTransformers, useTransformersDecimal, t }}
/>
);
};
ChartConfigure.Data = function Data() {
const { service } = useContext(ChartRendererContext);
const { current } = useContext(ChartConfigContext);
const fields = useFieldsWithAssociation();
const data = useData(current?.data);
const error = service?.error;
return !error ? (
<Table
dataSource={data.map((item, index) => ({ ...item, _key: index }))}
rowKey="_key"
scroll={{ x: 'max-content' }}
columns={Object.keys(data[0] || {}).map((col) => {
const field = getField(fields, col.split('.'));
return {
title: field?.label || col,
dataIndex: col,
key: col,
};
})}
size="small"
/>
) : (
<Alert
message="Error"
type="error"
description={error?.response?.data?.errors?.map?.((error: any) => error.message).join('\n') || error.message}
showIcon
/>
);
};