semantic-fe visual modeling pr (#21)

* [improvement][semantic-fe] Added an editing component to set filtering rules for Q&A. Now, the SQL editor will be accompanied by a list for display and control, to resolve ambiguity when using comma-separated values.
[improvement][semantic-fe] Improved validation logic and prompt copywriting for data source/dimension/metric editing and creation.
[improvement][semantic-fe] Improved user experience for visual modeling. Now, when using the legend to control the display/hide of data sources and their associated metric dimensions, the canvas will be re-layout based on the activated data source in the legend.

* [improvement][semantic-fe] Submitted a new version of the visual modeling tool.
[improvement][semantic-fe] Fixed an issue with the initialization of YoY and MoM metrics in Q&A settings.
[improvement][semantic-fe] Added a version field to the database settings.
[improvement][semantic-fe] 1. Added the ability to set YoY and MoM metrics in Q&A settings.2. Moved dimension value editing from the dimension editing window to the dimension list.

---------

Co-authored-by: tristanliu <tristanliu@tencent.com>
This commit is contained in:
tristanliu
2023-07-31 11:23:37 +08:00
committed by GitHub
parent e2b2d31429
commit 0ac652c5d9
50 changed files with 2375 additions and 1188 deletions

View File

@@ -1,31 +1,23 @@
import type { ActionType, ProColumns } from '@ant-design/pro-table';
import ProTable from '@ant-design/pro-table';
import { message, Button, Drawer, Space, Popconfirm, Modal, Card, Row, Col } from 'antd';
import { ConsoleSqlOutlined, CoffeeOutlined } from '@ant-design/icons';
import React, { useRef, useState, useEffect } from 'react';
import { message, Button, Space, Popconfirm } from 'antd';
import React, { useRef, useState } from 'react';
import type { Dispatch } from 'umi';
import { connect } from 'umi';
import DataSourceCreateForm from '../Datasource/components/DataSourceCreateForm';
import ClassDataSourceTypeModal from './ClassDataSourceTypeModal';
import type { StateType } from '../model';
import { getDatasourceList, deleteDatasource } from '../service';
import DataSource from '../Datasource';
import moment from 'moment';
const { Meta } = Card;
type Props = {
dispatch: Dispatch;
domainManger: StateType;
};
const ClassDataSourceTable: React.FC<Props> = ({ dispatch, domainManger }) => {
const { selectDomainId, dataBaseResultColsMap, dataBaseConfig } = domainManger;
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
const { selectDomainId } = domainManger;
const [dataSourceItem, setDataSourceItem] = useState<any>();
const [createDataSourceModalOpen, setCreateDataSourceModalOpen] = useState(false);
const [dataSourceModalVisible, setDataSourceModalVisible] = useState(false);
const [fastModeSql, setFastModeSql] = useState<string>('');
const [fastModeTableName, setFastModeTableName] = useState<string>('');
const actionRef = useRef<ActionType>();
@@ -70,11 +62,7 @@ const ClassDataSourceTable: React.FC<Props> = ({ dispatch, domainManger }) => {
key="classEditBtn"
onClick={() => {
setDataSourceItem(record);
if (record.datasourceDetail.queryType === 'table_query') {
setDataSourceModalVisible(true);
return;
}
setCreateModalVisible(true);
setCreateDataSourceModalOpen(true);
}}
>
@@ -133,25 +121,10 @@ const ClassDataSourceTable: React.FC<Props> = ({ dispatch, domainManger }) => {
return resData;
};
const queryDataBaseExcuteSql = (tableName: string) => {
const sql = `select * from ${tableName}`;
setFastModeSql(sql);
setFastModeTableName(tableName);
dispatch({
type: 'domainManger/queryDataBaseExcuteSql',
payload: {
sql,
domainId: selectDomainId,
tableName,
},
});
};
return (
<>
<ProTable
actionRef={actionRef}
headerTitle="数据源列表"
rowKey="id"
columns={columns}
params={{ domainId: selectDomainId }}
@@ -179,59 +152,12 @@ const ClassDataSourceTable: React.FC<Props> = ({ dispatch, domainManger }) => {
onCancel={() => {
setCreateDataSourceModalOpen(false);
}}
onTypeChange={(type) => {
if (type === 'fast') {
setDataSourceModalVisible(true);
} else {
setCreateModalVisible(true);
}
setCreateDataSourceModalOpen(false);
dataSourceItem={dataSourceItem}
onSubmit={() => {
actionRef.current?.reload();
}}
/>
}
{dataSourceModalVisible && (
<DataSourceCreateForm
sql={fastModeSql}
basicInfoFormMode="fast"
domainId={Number(selectDomainId)}
dataSourceItem={dataSourceItem}
onCancel={() => {
setDataSourceModalVisible(false);
}}
onDataBaseTableChange={(tableName: string) => {
queryDataBaseExcuteSql(tableName);
}}
onSubmit={() => {
setDataSourceModalVisible(false);
setDataSourceItem(undefined);
actionRef.current?.reload();
}}
createModalVisible={dataSourceModalVisible}
/>
)}
{createModalVisible && (
<Drawer
width={'100%'}
destroyOnClose
title="数据源编辑"
open={true}
onClose={() => {
setCreateModalVisible(false);
setDataSourceItem(undefined);
}}
footer={null}
>
<DataSource
initialValues={dataSourceItem}
domainId={Number(selectDomainId)}
onSubmitSuccess={() => {
setCreateModalVisible(false);
setDataSourceItem(undefined);
actionRef.current?.reload();
}}
/>
</Drawer>
)}
</>
);
};

View File

@@ -1,28 +1,69 @@
import { Modal, Card, Row, Col, Result, Button } from 'antd';
import { Button, Drawer, Result, Modal, Card, Row, Col } from 'antd';
import { ConsoleSqlOutlined, CoffeeOutlined } from '@ant-design/icons';
import React, { useState, useEffect } from 'react';
import { history, connect } from 'umi';
import type { Dispatch } from 'umi';
import { history, connect } from 'umi';
import DataSourceCreateForm from '../Datasource/components/DataSourceCreateForm';
import type { StateType } from '../model';
import DataSource from '../Datasource';
import { IDataSource } from '../data';
const { Meta } = Card;
type Props = {
open: boolean;
domainManger: StateType;
onTypeChange: (type: 'fast' | 'normal') => void;
dataSourceItem: IDataSource.IDataSourceItem;
onTypeChange?: (type: 'fast' | 'normal') => void;
onSubmit?: () => void;
onCancel?: () => void;
dispatch: Dispatch;
domainManger: StateType;
};
const ClassDataSourceTypeModal: React.FC<Props> = ({
open,
onTypeChange,
onSubmit,
dataSourceItem,
domainManger,
onCancel,
dispatch,
}) => {
const { selectDomainId, dataBaseConfig } = domainManger;
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
const [dataSourceModalVisible, setDataSourceModalVisible] = useState(false);
const [fastModeSql, setFastModeSql] = useState<string>('');
const [createDataSourceModalOpen, setCreateDataSourceModalOpen] = useState(false);
useEffect(() => {
setCreateDataSourceModalOpen(open);
}, [open]);
if (!dataSourceItem || !open) {
setCreateDataSourceModalOpen(open);
return;
}
if (dataSourceItem?.datasourceDetail?.queryType === 'table_query') {
setDataSourceModalVisible(true);
} else {
setCreateModalVisible(true);
}
}, [dataSourceItem, open]);
const queryDataBaseExcuteSql = (tableName: string) => {
const sql = `select * from ${tableName}`;
setFastModeSql(sql);
dispatch({
type: 'domainManger/queryDataBaseExcuteSql',
payload: {
sql,
domainId: selectDomainId,
tableName,
},
});
};
const handleCancel = () => {
onCancel?.();
};
return (
<>
@@ -30,7 +71,7 @@ const ClassDataSourceTypeModal: React.FC<Props> = ({
open={createDataSourceModalOpen}
onCancel={() => {
setCreateDataSourceModalOpen(false);
onCancel?.();
handleCancel();
}}
footer={null}
centered
@@ -43,8 +84,10 @@ const ClassDataSourceTypeModal: React.FC<Props> = ({
hoverable
style={{ height: 220 }}
onClick={() => {
onTypeChange('fast');
onTypeChange?.('fast');
setCreateDataSourceModalOpen(false);
setDataSourceModalVisible(true);
}}
cover={
<CoffeeOutlined
@@ -59,8 +102,10 @@ const ClassDataSourceTypeModal: React.FC<Props> = ({
<Col span={12}>
<Card
onClick={() => {
onTypeChange('normal');
onTypeChange?.('normal');
setCreateDataSourceModalOpen(false);
setCreateModalVisible(true);
}}
hoverable
style={{ height: 220 }}
@@ -93,10 +138,52 @@ const ClassDataSourceTypeModal: React.FC<Props> = ({
/>
)}
</Modal>
{dataSourceModalVisible && (
<DataSourceCreateForm
sql={fastModeSql}
basicInfoFormMode="fast"
domainId={Number(selectDomainId)}
dataSourceItem={dataSourceItem}
onCancel={() => {
setDataSourceModalVisible(false);
handleCancel();
}}
onDataBaseTableChange={(tableName: string) => {
queryDataBaseExcuteSql(tableName);
}}
onSubmit={() => {
setDataSourceModalVisible(false);
onSubmit?.();
}}
createModalVisible={dataSourceModalVisible}
/>
)}
{createModalVisible && (
<Drawer
width={'100%'}
destroyOnClose
title="数据源编辑"
open={true}
onClose={() => {
setCreateModalVisible(false);
handleCancel();
}}
footer={null}
>
<DataSource
initialValues={dataSourceItem}
domainId={Number(selectDomainId)}
onSubmitSuccess={() => {
setCreateModalVisible(false);
onSubmit?.();
}}
/>
</Drawer>
)}
</>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(ClassDataSourceTypeModal);

View File

@@ -0,0 +1,101 @@
import { Modal, Card, Row, Col, Result, Button } from 'antd';
import { ConsoleSqlOutlined, CoffeeOutlined } from '@ant-design/icons';
import React, { useState, useEffect } from 'react';
import { history, connect } from 'umi';
import type { StateType } from '../model';
const { Meta } = Card;
type Props = {
open: boolean;
domainManger: StateType;
onTypeChange: (type: 'fast' | 'normal') => void;
onCancel?: () => void;
};
const ClassDataSourceTypeModal: React.FC<Props> = ({
open,
onTypeChange,
domainManger,
onCancel,
}) => {
const { selectDomainId, dataBaseConfig } = domainManger;
const [createDataSourceModalOpen, setCreateDataSourceModalOpen] = useState(false);
useEffect(() => {
setCreateDataSourceModalOpen(open);
}, [open]);
return (
<>
<Modal
open={createDataSourceModalOpen}
onCancel={() => {
setCreateDataSourceModalOpen(false);
onCancel?.();
}}
footer={null}
centered
closable={false}
>
{dataBaseConfig && dataBaseConfig.id ? (
<Row gutter={16} style={{ marginTop: '0px' }}>
<Col span={12}>
<Card
hoverable
style={{ height: 220 }}
onClick={() => {
onTypeChange('fast');
setCreateDataSourceModalOpen(false);
}}
cover={
<CoffeeOutlined
width={240}
style={{ paddingTop: '45px', height: 120, fontSize: '48px', color: '#1890ff' }}
/>
}
>
<Meta title="快速创建" description="自动进行数据源可视化创建" />
</Card>
</Col>
<Col span={12}>
<Card
onClick={() => {
onTypeChange('normal');
setCreateDataSourceModalOpen(false);
}}
hoverable
style={{ height: 220 }}
cover={
<ConsoleSqlOutlined
style={{ paddingTop: '45px', height: 120, fontSize: '48px', color: '#1890ff' }}
/>
}
>
<Meta title="SQL脚本" description="自定义SQL脚本创建数据源" />
</Card>
</Col>
</Row>
) : (
<Result
status="warning"
subTitle="创建数据源需要先完成数据库设置"
extra={
<Button
type="primary"
key="console"
onClick={() => {
history.replace(`/semanticModel/${selectDomainId}/dataBase`);
onCancel?.();
}}
>
</Button>
}
/>
)}
</Modal>
</>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(ClassDataSourceTypeModal);

View File

@@ -8,6 +8,7 @@ import type { StateType } from '../model';
import { SENSITIVE_LEVEL_ENUM } from '../constant';
import { getDatasourceList, getDimensionList, deleteDimension } from '../service';
import DimensionInfoModal from './DimensionInfoModal';
import DimensionValueSettingModal from './DimensionValueSettingModal';
import { ISemantic } from '../data';
import moment from 'moment';
import styles from './style.less';
@@ -22,6 +23,11 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
const [dimensionItem, setDimensionItem] = useState<ISemantic.IDimensionItem>();
const [dataSourceList, setDataSourceList] = useState<any[]>([]);
const [dimensionValueSettingList, setDimensionValueSettingList] = useState<
ISemantic.IDimensionValueSettingItem[]
>([]);
const [dimensionValueSettingModalVisible, setDimensionValueSettingModalVisible] =
useState<boolean>(false);
const [pagination, setPagination] = useState({
current: 1,
pageSize: 20,
@@ -141,6 +147,20 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
>
</a>
<a
key="classEditBtn"
onClick={() => {
setDimensionItem(record);
setDimensionValueSettingModalVisible(true);
if (Array.isArray(record.dimValueMaps)) {
setDimensionValueSettingList(record.dimValueMaps);
} else {
setDimensionValueSettingList([]);
}
}}
>
</a>
<Popconfirm
title="确认删除?"
okText="是"
@@ -175,7 +195,7 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
<ProTable
className={`${styles.classTable} ${styles.classTableSelectColumnAlignLeft}`}
actionRef={actionRef}
headerTitle="维度列表"
// headerTitle="维度列表"
rowKey="id"
columns={columns}
request={queryDimensionList}
@@ -236,6 +256,26 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
}}
/>
)}
{dimensionValueSettingModalVisible && (
<DimensionValueSettingModal
dimensionValueSettingList={dimensionValueSettingList}
open={dimensionValueSettingModalVisible}
dimensionItem={dimensionItem}
onCancel={() => {
setDimensionValueSettingModalVisible(false);
}}
onSubmit={() => {
actionRef?.current?.reload();
dispatch({
type: 'domainManger/queryDimensionList',
payload: {
domainId: selectDomainId,
},
});
setDimensionValueSettingModalVisible(false);
}}
/>
)}
</>
);
};

View File

@@ -166,43 +166,12 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
},
];
// const saveMetric = async (fieldsValue: any, reloadState: boolean = true) => {
// const queryParams = {
// domainId: selectDomainId,
// ...fieldsValue,
// };
// if (queryParams.typeParams && !queryParams.typeParams.expr) {
// message.error('度量表达式不能为空');
// return;
// }
// let saveMetricQuery = creatExprMetric;
// if (queryParams.id) {
// saveMetricQuery = updateExprMetric;
// }
// const { code, msg } = await saveMetricQuery(queryParams);
// if (code === 200) {
// message.success('编辑指标成功');
// setCreateModalVisible(false);
// if (reloadState) {
// actionRef?.current?.reload();
// }
// dispatch({
// type: 'domainManger/queryMetricList',
// payload: {
// domainId: selectDomainId,
// },
// });
// return;
// }
// message.error(msg);
// };
return (
<>
<ProTable
className={`${styles.classTable} ${styles.classTableSelectColumnAlignLeft}`}
actionRef={actionRef}
headerTitle="指标列表"
// headerTitle="指标列表"
rowKey="id"
search={{
span: 4,

View File

@@ -0,0 +1,166 @@
import React, { useState, useEffect } from 'react';
import { List, Collapse, Button, Input } from 'antd';
import { uuid } from '@/utils/utils';
import styles from './style.less';
const { Panel } = Collapse;
const { TextArea } = Input;
type Props = {
title?: string;
defaultCollapse?: boolean;
value?: string[];
onChange?: (list: string[]) => void;
};
type ListItem = {
id: string;
sql: string;
};
type List = ListItem[];
const CommonEditList: React.FC<Props> = ({ title, defaultCollapse = false, value, onChange }) => {
const [listDataSource, setListDataSource] = useState<List>([]);
const [currentSql, setCurrentSql] = useState<string>('');
const [activeKey, setActiveKey] = useState<string>();
const [currentRecord, setCurrentRecord] = useState<ListItem>();
useEffect(() => {
if (Array.isArray(value)) {
const list = value.map((sql: string) => {
return {
id: uuid(),
sql,
};
});
setListDataSource(list);
}
}, [value]);
const handleListChange = (listDataSource: List) => {
const sqlList = listDataSource.map((item) => {
return item.sql;
});
onChange?.(sqlList);
};
return (
<div className={styles.commonEditList}>
<Collapse
activeKey={activeKey}
defaultActiveKey={defaultCollapse ? ['editor'] : undefined}
onChange={() => {}}
ghost
>
<Panel
header={title}
key="editor"
extra={
activeKey ? (
<Button
key="saveBtn"
type="primary"
onClick={() => {
if (!currentRecord && !currentSql) {
setActiveKey(undefined);
return;
}
if (currentRecord) {
const list = [...listDataSource].map((item) => {
if (item.id === currentRecord.id) {
return {
...item,
sql: currentSql,
};
}
return item;
});
setListDataSource(list);
handleListChange(list);
} else {
const list = [
...listDataSource,
{
id: uuid(),
sql: currentSql,
},
];
setListDataSource(list);
handleListChange(list);
}
setActiveKey(undefined);
}}
>
</Button>
) : (
<Button
type="primary"
key="createBtn"
onClick={() => {
setCurrentRecord(undefined);
setCurrentSql('');
setActiveKey('editor');
}}
>
</Button>
)
}
showArrow={false}
>
<div>
<TextArea
placeholder="请输入推荐问题"
value={currentSql}
style={{ height: 150 }}
minLength={5}
onChange={(e) => {
setCurrentSql(e.target.value);
}}
/>
</div>
</Panel>
</Collapse>
<List
itemLayout="horizontal"
dataSource={listDataSource || []}
renderItem={(item) => (
<List.Item
actions={[
<a
key="list-loadmore-edit"
onClick={() => {
setCurrentSql(item.sql);
setCurrentRecord(item);
setActiveKey('editor');
}}
>
</a>,
<a
key="list-loadmore-more"
onClick={() => {
const list = [...listDataSource].filter(({ id }) => {
return item.id !== id;
});
handleListChange(list);
setListDataSource(list);
}}
>
</a>,
]}
>
<List.Item.Meta title={item.sql} />
</List.Item>
)}
/>
</div>
);
};
export default CommonEditList;

View File

@@ -9,7 +9,7 @@ type Props = {
title?: string;
tableDataSource: any[];
columnList: any[];
rowKey: string;
rowKey?: string;
editableProTableProps?: any;
onDataSourceChange?: (dataSource: any) => void;
extenderCtrlColumn?: (text, record, _, action) => ReactNode[];
@@ -35,6 +35,7 @@ const CommonEditTable: React.FC<Props> = forwardRef(
}: Props,
ref: Ref<any>,
) => {
const defaultRowKey = rowKey || 'editRowId';
const [dataSource, setDataSource] = useState<any[]>(tableDataSource);
const actionRef = useRef<ActionType>();
@@ -50,7 +51,7 @@ const CommonEditTable: React.FC<Props> = forwardRef(
tableDataSource.map((item: any) => {
return {
...item,
editRowId: item[rowKey] || (Math.random() * 1000000).toFixed(0),
editRowId: item[defaultRowKey] || (Math.random() * 1000000).toFixed(0),
};
}),
);
@@ -82,7 +83,9 @@ const CommonEditTable: React.FC<Props> = forwardRef(
<a
key="deleteBtn"
onClick={() => {
const data = [...dataSource].filter((item) => item[rowKey] !== record[rowKey]);
const data = [...dataSource].filter(
(item) => item[defaultRowKey] !== record[defaultRowKey],
);
setDataSource(data);
handleDataSourceChange(data);
}}
@@ -111,7 +114,7 @@ const CommonEditTable: React.FC<Props> = forwardRef(
key={title}
actionRef={actionRef}
headerTitle={title}
rowKey={'editRowId'}
rowKey={defaultRowKey}
columns={columns}
value={dataSource}
tableAlertRender={() => {
@@ -133,9 +136,9 @@ const CommonEditTable: React.FC<Props> = forwardRef(
}}
editable={{
onSave: (_, row) => {
const rowKeyValue = row[rowKey];
const rowKeyValue = row[defaultRowKey];
const isSame = dataSource.filter((item: any, index: number) => {
return index !== row.index && item[rowKey] === rowKeyValue;
return index !== row.index && item[defaultRowKey] === rowKeyValue;
});
if (isSame[0]) {
message.error('存在重复值');

View File

@@ -20,15 +20,8 @@ const DatabaseCreateForm: ForwardRefRenderFunction<any, Props> = (
) => {
const [form] = Form.useForm();
const [selectedDbType, setSelectedDbType] = useState<string>('h2');
// const queryDatabaseConfig = async () => {
// const { code, data } = await getDatabaseByDomainId(domainId);
// if (code === 200) {
// form.setFieldsValue({ ...data });
// setSelectedDbType(data?.type);
// return;
// }
// message.error('数据库配置获取错误');
// };
const [testLoading, setTestLoading] = useState<boolean>(false);
useEffect(() => {
form.resetFields();
@@ -36,11 +29,6 @@ const DatabaseCreateForm: ForwardRefRenderFunction<any, Props> = (
setSelectedDbType(dataBaseConfig?.type);
}, [dataBaseConfig]);
// useEffect(() => {
// form.resetFields();
// // queryDatabaseConfig();
// }, [domainId]);
const getFormValidateFields = async () => {
return await form.validateFields();
};
@@ -65,10 +53,12 @@ const DatabaseCreateForm: ForwardRefRenderFunction<any, Props> = (
};
const testDatabaseConnection = async () => {
const values = await form.validateFields();
setTestLoading(true);
const { code, data } = await testDatabaseConnect({
...values,
domainId,
});
setTestLoading(false);
if (code === 200 && data) {
message.success('连接测试通过');
return;
@@ -138,7 +128,9 @@ const DatabaseCreateForm: ForwardRefRenderFunction<any, Props> = (
<FormItem name="database" label="数据库名称">
<Input placeholder="请输入数据库名称" />
</FormItem>
<FormItem name="version" label="数据库版本">
<Input placeholder="请输入数据库版本" />
</FormItem>
<FormItem name="description" label="描述">
<TextArea placeholder="请输入数据库描述" style={{ height: 100 }} />
</FormItem>
@@ -146,6 +138,7 @@ const DatabaseCreateForm: ForwardRefRenderFunction<any, Props> = (
<Space>
<Button
type="primary"
loading={testLoading}
onClick={() => {
testDatabaseConnection();
}}

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { Button, Form, Input, Modal, Select } from 'antd';
import { SENSITIVE_LEVEL_OPTIONS } from '../constant';
import { formLayout } from '@/components/FormHelper/utils';
@@ -6,6 +6,7 @@ import SqlEditor from '@/components/SqlEditor';
import InfoTagList from './InfoTagList';
import { ISemantic } from '../data';
import { createDimension, updateDimension } from '../service';
// import DimensionValueSettingModal from './DimensionValueSettingModal';
import { message } from 'antd';
export type CreateFormProps = {
@@ -31,16 +32,28 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
onSubmit: handleUpdate,
}) => {
const isEdit = !!dimensionItem?.id;
const [dimensionValueSettingList, setDimensionValueSettingList] = useState<
ISemantic.IDimensionValueSettingItem[]
>([]);
const [form] = Form.useForm();
const { setFieldsValue, resetFields } = form;
const handleSubmit = async () => {
// const [dimensionValueSettingModalVisible, setDimensionValueSettingModalVisible] =
// useState<boolean>(false);
const handleSubmit = async (
isSilenceSubmit = false,
dimValueMaps?: ISemantic.IDimensionValueSettingItem[],
) => {
const fieldsValue = await form.validateFields();
await saveDimension(fieldsValue);
await saveDimension(
{
...fieldsValue,
dimValueMaps: dimValueMaps || dimensionValueSettingList,
},
isSilenceSubmit,
);
};
const saveDimension = async (fieldsValue: any) => {
const saveDimension = async (fieldsValue: any, isSilenceSubmit = false) => {
const queryParams = {
domainId,
type: 'categorical',
@@ -52,8 +65,10 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
}
const { code, msg } = await saveDimensionQuery(queryParams);
if (code === 200) {
message.success('编辑维度成功');
handleUpdate(fieldsValue);
if (!isSilenceSubmit) {
message.success('编辑维度成功');
handleUpdate(fieldsValue);
}
return;
}
message.error(msg);
@@ -66,6 +81,11 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
useEffect(() => {
if (dimensionItem) {
setFormVal();
if (Array.isArray(dimensionItem.dimValueMaps)) {
setDimensionValueSettingList(dimensionItem.dimValueMaps);
} else {
setDimensionValueSettingList([]);
}
} else {
resetFields();
}
@@ -78,7 +98,12 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
return (
<>
<Button onClick={onCancel}></Button>
<Button type="primary" onClick={handleSubmit}>
<Button
type="primary"
onClick={() => {
handleSubmit();
}}
>
</Button>
</>
@@ -93,20 +118,22 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
</FormItem>
<FormItem
name="name"
label="维度中文名"
rules={[{ required: true, message: '请输入维度中文名' }]}
label="维度名"
rules={[{ required: true, message: '请输入维度名' }]}
>
<Input placeholder="名称不可重复" />
</FormItem>
<FormItem
hidden={isEdit}
name="bizName"
label="维度英文名"
rules={[{ required: true, message: '请输入维度英文名' }]}
label="字段名称"
rules={[{ required: true, message: '请输入字段名称' }]}
>
<Input placeholder="名称不可重复" disabled={isEdit} />
</FormItem>
<FormItem
hidden={isEdit}
name="datasourceId"
label="所属数据源"
rules={[{ required: true, message: '请选择所属数据源' }]}
@@ -158,6 +185,16 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
>
<TextArea placeholder="请输入维度描述" />
</FormItem>
{/* <FormItem name="dimValueMaps" label="维度值设置">
<Button
type="primary"
onClick={() => {
setDimensionValueSettingModalVisible(true);
}}
>
设置
</Button>
</FormItem> */}
<FormItem
name="expr"
label="表达式"
@@ -171,28 +208,38 @@ const DimensionInfoModal: React.FC<CreateFormProps> = ({
};
return (
<Modal
width={800}
destroyOnClose
title="维度信息"
style={{ top: 48 }}
maskClosable={false}
open={bindModalVisible}
footer={renderFooter()}
onCancel={onCancel}
>
<Form
{...formLayout}
form={form}
initialValues={
{
// ...formVals,
}
}
<>
<Modal
width={800}
destroyOnClose
title="维度信息"
style={{ top: 48 }}
maskClosable={false}
open={bindModalVisible}
footer={renderFooter()}
onCancel={onCancel}
>
{renderContent()}
</Form>
</Modal>
<Form {...formLayout} form={form}>
{renderContent()}
</Form>
</Modal>
{/* {dimensionValueSettingModalVisible && (
<DimensionValueSettingModal
dimensionValueSettingList={dimensionValueSettingList}
open={dimensionValueSettingModalVisible}
onCancel={() => {
setDimensionValueSettingModalVisible(false);
}}
onSubmit={(dimValueMaps) => {
if (isEdit) {
handleSubmit(true, dimValueMaps);
}
setDimensionValueSettingList(dimValueMaps);
setDimensionValueSettingModalVisible(false);
}}
/>
)} */}
</>
);
};

View File

@@ -0,0 +1,156 @@
import React, { useEffect, useState } from 'react';
import { Button, Modal, message } from 'antd';
import { ISemantic } from '../data';
import CommonEditTable from './CommonEditTable';
import { createDimension, updateDimension } from '../service';
import { connect } from 'umi';
import type { StateType } from '../model';
export type CreateFormProps = {
dimensionValueSettingList: ISemantic.IDimensionValueSettingItem[];
onCancel: () => void;
dimensionItem?: ISemantic.IDimensionItem;
open: boolean;
onSubmit: (values?: any) => void;
domainManger: StateType;
};
type TableDataSource = { techName: string; bizName: string; alias: string };
const DimensionInfoModal: React.FC<CreateFormProps> = ({
onCancel,
open,
dimensionItem,
dimensionValueSettingList,
domainManger,
onSubmit,
}) => {
const [tableDataSource, setTableDataSource] = useState<TableDataSource[]>([]);
const { selectDomainId } = domainManger;
const [dimValueMaps, setDimValueMaps] = useState<ISemantic.IDimensionValueSettingItem[]>([]);
useEffect(() => {
const dataSource = dimensionValueSettingList.map((item) => {
const { alias } = item;
return {
...item,
alias: Array.isArray(alias) ? alias.join(',') : '',
};
});
setTableDataSource(dataSource);
setDimValueMaps(dimensionValueSettingList);
}, [dimensionValueSettingList]);
const handleSubmit = async () => {
await saveDimension({ dimValueMaps });
onSubmit?.(dimValueMaps);
};
const saveDimension = async (fieldsValue: any) => {
if (!dimensionItem?.id) {
return;
}
const queryParams = {
domainId: selectDomainId,
id: dimensionItem.id,
...fieldsValue,
};
const { code, msg } = await updateDimension(queryParams);
if (code === 200) {
return;
}
message.error(msg);
};
const renderFooter = () => {
return (
<>
<Button onClick={onCancel}></Button>
<Button
type="primary"
onClick={() => {
handleSubmit();
}}
>
</Button>
</>
);
};
const columns = [
{
title: '技术名称',
dataIndex: 'techName',
width: 200,
formItemProps: {
fieldProps: {
placeholder: '请填写技术名称',
},
rules: [
{
required: true,
whitespace: true,
message: '此项是必填项',
},
],
},
},
{
title: '业务名称',
dataIndex: 'bizName',
width: 200,
fieldProps: {
placeholder: '请填写业务名称',
},
formItemProps: {
rules: [
{
required: true,
whitespace: true,
message: '此项是必填项',
},
],
},
},
{
title: '别名',
dataIndex: 'alias',
fieldProps: {
placeholder: '多个别名用英文逗号隔开',
},
},
];
return (
<Modal
width={1000}
destroyOnClose
title="维度值设置"
style={{ top: 48 }}
maskClosable={false}
open={open}
footer={renderFooter()}
onCancel={onCancel}
>
<CommonEditTable
tableDataSource={tableDataSource}
columnList={columns}
onDataSourceChange={(tableData) => {
const dimValueMaps = tableData.map((item: TableDataSource) => {
return {
...item,
alias: item.alias ? `${item.alias}`.split(',') : [],
};
});
setDimValueMaps(dimValueMaps);
}}
/>
</Modal>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(DimensionInfoModal);

View File

@@ -11,7 +11,7 @@ import {
} from './utils';
import styles from '../style.less';
import { ISemantic } from '../../data';
import { ChatConfigType, TransType } from '../../enum';
import { ChatConfigType, TransType, SemanticNodeType } from '../../enum';
import TransTypeTag from '../TransTypeTag';
type Props = {
@@ -99,7 +99,7 @@ const DefaultSettingForm: ForwardRefRenderFunction<any, Props> = (
name,
label: (
<>
<TransTypeTag type={TransType.DIMENSION} />
<TransTypeTag type={SemanticNodeType.DIMENSION} />
{name}
</>
),
@@ -115,7 +115,7 @@ const DefaultSettingForm: ForwardRefRenderFunction<any, Props> = (
name,
label: (
<>
<TransTypeTag type={TransType.METRIC} />
<TransTypeTag type={SemanticNodeType.METRIC} />
{name}
</>
),
@@ -198,30 +198,56 @@ const DefaultSettingForm: ForwardRefRenderFunction<any, Props> = (
</FormItem>
)}
{chatConfigType === ChatConfigType.AGG && (
<FormItem
name="metricIds"
label={
<FormItemTitle
title={'指标'}
subTitle={'问答搜索结果选择中,如果没有指定指标,将会采用默认指标进行展示'}
<>
{/* <FormItem
name="metricIds"
label={
<FormItemTitle
title={'指标'}
subTitle={'问答搜索结果选择中,如果没有指定指标,将会采用默认指标进行展示'}
/>
}
>
<Select
mode="multiple"
allowClear
style={{ width: '100%' }}
filterOption={(inputValue: string, item: any) => {
const { label } = item;
if (label.includes(inputValue)) {
return true;
}
return false;
}}
placeholder="请选择展示指标信息"
options={metricListOptions}
/>
}
>
<Select
mode="multiple"
allowClear
style={{ width: '100%' }}
filterOption={(inputValue: string, item: any) => {
const { label } = item;
if (label.includes(inputValue)) {
return true;
}
return false;
}}
placeholder="请选择展示指标信息"
options={metricListOptions}
/>
</FormItem>
</FormItem>
<FormItem
name="ratioMetricIds"
label={
<FormItemTitle
title={'同环比指标'}
subTitle={'问答搜索含有指定的指标,将会同时计算该指标最后一天的同环比'}
/>
}
>
<Select
mode="multiple"
allowClear
style={{ width: '100%' }}
filterOption={(inputValue: string, item: any) => {
const { label } = item;
if (label.includes(inputValue)) {
return true;
}
return false;
}}
placeholder="请选择同环比指标"
options={metricListOptions}
/>
</FormItem> */}
</>
)}
<FormItem

View File

@@ -15,7 +15,7 @@ type Props = {
settingSourceList: any[];
onCancel: () => void;
visible: boolean;
onSubmit: (params?: any) => void;
onSubmit: (params?: { isSilenceSubmit?: boolean }) => void;
};
const dimensionConfig = {
@@ -72,7 +72,8 @@ const DimensionAndMetricVisibleModal: React.FC<Props> = ({
}
}, [entityData, settingSourceList]);
const saveEntity = async () => {
const saveEntity = async (submitData: any, isSilenceSubmit = false) => {
const { selectedKeyList, knowledgeInfosMap } = submitData;
const globalKnowledgeConfigFormFields = await formRef?.current?.getFormValidateFields?.();
let globalKnowledgeConfig = entityData.globalKnowledgeConfig;
if (globalKnowledgeConfigFormFields) {
@@ -127,8 +128,10 @@ const DimensionAndMetricVisibleModal: React.FC<Props> = ({
id,
});
if (code === 200) {
onSubmit?.();
message.success('保存成功');
if (!isSilenceSubmit) {
message.success('保存成功');
}
onSubmit?.({ isSilenceSubmit });
return;
}
message.error(msg);
@@ -145,7 +148,7 @@ const DimensionAndMetricVisibleModal: React.FC<Props> = ({
<Button
type="primary"
onClick={() => {
saveEntity();
saveEntity({ selectedKeyList, knowledgeInfosMap });
}}
>
@@ -160,8 +163,9 @@ const DimensionAndMetricVisibleModal: React.FC<Props> = ({
key: 'visibleSetting',
children: (
<DimensionMetricVisibleTransfer
onKnowledgeInfosMapChange={(knowledgeInfosMap) => {
setKnowledgeInfosMap(knowledgeInfosMap);
onKnowledgeInfosMapChange={(knowledgeInfos) => {
setKnowledgeInfosMap(knowledgeInfos);
saveEntity({ selectedKeyList, knowledgeInfosMap: knowledgeInfos }, true);
}}
knowledgeInfosMap={knowledgeInfosMap}
titles={settingTypeConfig.titles}
@@ -169,6 +173,7 @@ const DimensionAndMetricVisibleModal: React.FC<Props> = ({
targetList={selectedKeyList}
onChange={(newTargetKeys) => {
handleTransferChange(newTargetKeys);
saveEntity({ selectedKeyList: newTargetKeys, knowledgeInfosMap }, true);
}}
/>
),
@@ -177,10 +182,12 @@ const DimensionAndMetricVisibleModal: React.FC<Props> = ({
label: '全局维度值过滤',
key: 'dimensionValueFilter',
children: (
<DimensionValueSettingForm
initialValues={globalKnowledgeConfigInitialValues}
ref={formRef}
/>
<div style={{ margin: '0 auto', width: '975px' }}>
<DimensionValueSettingForm
initialValues={globalKnowledgeConfigInitialValues}
ref={formRef}
/>
</div>
),
},
];

View File

@@ -74,9 +74,11 @@ const DimensionMetricVisibleForm: ForwardRefRenderFunction<any, Props> = ({
onCancel={() => {
setDimensionModalVisible(false);
}}
onSubmit={() => {
onSubmit={(params) => {
onSubmit?.();
setDimensionModalVisible(false);
if (!params?.isSilenceSubmit) {
setDimensionModalVisible(false);
}
}}
/>
)}

View File

@@ -5,8 +5,7 @@ import { Form, Input } from 'antd';
import { formLayout } from '@/components/FormHelper/utils';
import { isString } from 'lodash';
import styles from '../style.less';
import CommonEditList from '../../components/CommonEditList/index';
import SqlEditor from '@/components/SqlEditor';
import CommonEditList from '../../components/CommonEditList';
type Props = {
initialValues: any;
onSubmit?: () => void;

View File

@@ -1,14 +1,13 @@
import { useEffect, useState, forwardRef, useImperativeHandle } from 'react';
import type { ForwardRefRenderFunction } from 'react';
import { message, Form, Input, Select, Button } from 'antd';
import { addDomainExtend, editDomainExtend } from '../../service';
import type { ISemantic, IChatConfig } from '../../data';
import { updateDomain } from '../../service';
import type { ISemantic } from '../../data';
import { formLayout } from '@/components/FormHelper/utils';
import { formatRichEntityDataListToIds } from './utils';
import styles from '../style.less';
type Props = {
entityData: IChatConfig.IChatRichConfig;
entityData?: { id: number; names: string[] };
dimensionList: ISemantic.IDimensionList;
domainId: number;
onSubmit: () => void;
@@ -21,22 +20,20 @@ const EntityCreateForm: ForwardRefRenderFunction<any, Props> = (
ref,
) => {
const [form] = Form.useForm();
const formatEntityData = formatRichEntityDataListToIds(entityData);
const [dimensionListOptions, setDimensionListOptions] = useState<any>([]);
const getFormValidateFields = async () => {
return await form.validateFields();
};
useEffect(() => {
form.resetFields();
if (!entityData?.entity) {
if (!entityData) {
return;
}
form.setFieldsValue({
...formatEntityData.entity,
id: formatEntityData.id,
...entityData,
name: entityData.names.join(','),
});
}, [entityData]);
@@ -56,20 +53,14 @@ const EntityCreateForm: ForwardRefRenderFunction<any, Props> = (
const saveEntity = async () => {
const values = await form.validateFields();
const { id, name } = values;
let saveDomainExtendQuery = addDomainExtend;
if (id) {
saveDomainExtendQuery = editDomainExtend;
}
const { code, msg, data } = await saveDomainExtendQuery({
chatDetailConfig: {
...formatEntityData,
entity: {
...values,
names: name.split(','),
},
const { name } = values;
const { code, msg, data } = await updateDomain({
entity: {
...values,
names: name.split(','),
},
id,
id: domainId,
domainId,
});

View File

@@ -5,7 +5,7 @@ import { connect } from 'umi';
import type { StateType } from '../../model';
import { getDomainExtendDetailConfig } from '../../service';
import ProCard from '@ant-design/pro-card';
import EntityCreateForm from './EntityCreateForm';
import DefaultSettingForm from './DefaultSettingForm';
import type { IChatConfig } from '../../data';
import DimensionMetricVisibleForm from './DimensionMetricVisibleForm';
@@ -26,8 +26,6 @@ const EntitySection: React.FC<Props> = ({
const [entityData, setentityData] = useState<IChatConfig.IChatRichConfig>();
const entityCreateRef = useRef<any>({});
const queryThemeListData: any = async () => {
const { code, data } = await getDomainExtendDetailConfig({
domainId: selectDomainId,
@@ -58,25 +56,6 @@ const EntitySection: React.FC<Props> = ({
return (
<div style={{ width: 800, margin: '0 auto' }}>
<Space direction="vertical" style={{ width: '100%' }} size={20}>
{chatConfigType === 'detail' && entityData && (
<ProCard title="实体" bordered>
<EntityCreateForm
ref={entityCreateRef}
domainId={Number(selectDomainId)}
entityData={entityData}
dimensionList={dimensionList.filter((item) => {
const blackDimensionList = entityData?.visibility?.blackDimIdList;
if (Array.isArray(blackDimensionList)) {
return !blackDimensionList.includes(item.id);
}
return false;
})}
onSubmit={() => {
queryThemeListData();
}}
/>
</ProCard>
)}
<ProCard bordered title="问答可见">
<DimensionMetricVisibleForm
chatConfigKey={

View File

@@ -0,0 +1,69 @@
import { message, Space } from 'antd';
import React, { useState, useEffect, useRef } from 'react';
import type { Dispatch } from 'umi';
import { connect } from 'umi';
import type { StateType } from '../../model';
import { getDomainDetail } from '../../service';
import ProCard from '@ant-design/pro-card';
import EntityCreateForm from './EntityCreateForm';
import type { IChatConfig } from '../../data';
type Props = {
dispatch: Dispatch;
domainManger: StateType;
};
const EntitySettingSection: React.FC<Props> = ({ domainManger }) => {
const { selectDomainId, dimensionList, metricList } = domainManger;
const [entityData, setEntityData] = useState<IChatConfig.IChatRichConfig>();
const entityCreateRef = useRef<any>({});
const queryDomainData: any = async () => {
const { code, data } = await getDomainDetail({
domainId: selectDomainId,
});
if (code === 200) {
const { entity } = data;
setEntityData(entity);
return;
}
message.error('获取问答设置信息失败');
};
const initPage = async () => {
queryDomainData();
};
useEffect(() => {
initPage();
}, [selectDomainId]);
return (
<div style={{ width: 800, margin: '0 auto' }}>
<Space direction="vertical" style={{ width: '100%' }} size={20}>
{
<ProCard title="实体" bordered>
<EntityCreateForm
ref={entityCreateRef}
domainId={Number(selectDomainId)}
entityData={entityData}
dimensionList={dimensionList}
onSubmit={() => {
queryDomainData();
}}
/>
</ProCard>
}
</Space>
</div>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(EntitySettingSection);

View File

@@ -0,0 +1,87 @@
import { message } from 'antd';
import React, { useState, useEffect } from 'react';
import { connect } from 'umi';
import type { StateType } from '../../model';
import { getDomainExtendConfig, addDomainExtend, editDomainExtend } from '../../service';
import ProCard from '@ant-design/pro-card';
import TextAreaCommonEditList from '../../components/CommonEditList/TextArea';
type Props = {
domainManger: StateType;
};
const RecommendedQuestionsSection: React.FC<Props> = ({ domainManger }) => {
const { selectDomainId } = domainManger;
const [questionData, setQuestionData] = useState<string[]>([]);
const [currentRecordId, setCurrentRecordId] = useState<number>(0);
const queryThemeListData: any = async () => {
const { code, data } = await getDomainExtendConfig({
domainId: selectDomainId,
});
if (code === 200) {
const target = data?.[0] || {};
if (Array.isArray(target.recommendedQuestions)) {
setQuestionData(
target.recommendedQuestions.map((item: { question: string }) => {
return item.question;
}),
);
setCurrentRecordId(target.id || 0);
} else {
setQuestionData([]);
setCurrentRecordId(0);
}
return;
}
message.error('获取问答设置信息失败');
};
const saveEntity = async (list: string[]) => {
let saveDomainExtendQuery = addDomainExtend;
if (currentRecordId) {
saveDomainExtendQuery = editDomainExtend;
}
const { code, msg } = await saveDomainExtendQuery({
recommendedQuestions: list.map((question: string) => {
return { question };
}),
id: currentRecordId,
domainId: selectDomainId,
});
if (code === 200) {
return;
}
message.error(msg);
};
const initPage = async () => {
queryThemeListData();
};
useEffect(() => {
initPage();
}, [selectDomainId]);
return (
<div style={{ width: 800, margin: '0 auto' }}>
<ProCard bordered title="问题推荐列表">
<TextAreaCommonEditList
value={questionData}
onChange={(list) => {
saveEntity(list);
setQuestionData(list);
}}
/>
</ProCard>
</div>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(RecommendedQuestionsSection);

View File

@@ -18,8 +18,9 @@ export const formatRichEntityDataListToIds = (
const detailData: {
dimensionIds: number[];
metricIds: number[];
} = { dimensionIds: [], metricIds: [] };
const { dimensions, metrics } = chatDefaultConfig || {};
ratioMetricIds: number[];
} = { dimensionIds: [], metricIds: [], ratioMetricIds: [] };
const { dimensions, metrics, ratioMetrics } = chatDefaultConfig || {};
if (Array.isArray(dimensions)) {
detailData.dimensionIds = dimensions.map((item: ISemantic.IDimensionItem) => {
return item.id;
@@ -30,6 +31,12 @@ export const formatRichEntityDataListToIds = (
return item.id;
});
}
if (Array.isArray(ratioMetrics)) {
detailData.ratioMetricIds = ratioMetrics.map((item: ISemantic.IMetricItem) => {
return item.id;
});
}
let entitySetting = {};
if (entity) {
const entityItem = entity.dimItem;

View File

@@ -199,15 +199,15 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
</FormItem>
<FormItem
name="name"
label="指标中文名"
rules={[{ required: true, message: '请输入指标中文名' }]}
label="指标名"
rules={[{ required: true, message: '请输入指标名' }]}
>
<Input placeholder="名称不可重复" />
</FormItem>
<FormItem
name="bizName"
label="指标英文名"
rules={[{ required: true, message: '请输入指标英文名' }]}
label="字段名称"
rules={[{ required: true, message: '请输入字段名称' }]}
>
<Input placeholder="名称不可重复" disabled={isEdit} />
</FormItem>

View File

@@ -1,19 +1,21 @@
import { Tag } from 'antd';
import React from 'react';
import { TransType } from '../enum';
import { SemanticNodeType } from '../enum';
type Props = {
type: TransType;
type: SemanticNodeType;
};
const TransTypeTag: React.FC<Props> = ({ type }) => {
return (
<>
{type === TransType.DIMENSION ? (
{type === SemanticNodeType.DIMENSION ? (
<Tag color="blue">{'维度'}</Tag>
) : type === 'metric' ? (
) : type === SemanticNodeType.METRIC ? (
<Tag color="orange">{'指标'}</Tag>
) : type === SemanticNodeType.DATASOURCE ? (
<Tag color="green">{'数据源'}</Tag>
) : (
<></>
)}

View File

@@ -8,7 +8,8 @@
.projectManger {
width: 100%;
min-height: calc(100vh - 48px);
background: #f8f9fb;
// background: #f8f9fb;
background-color: #fff;
position: relative;
@@ -37,8 +38,17 @@
}
.tab {
padding: 0 20px;
line-height: 28px;
:global {
.ant-tabs-tab-btn {
font-size: 16px !important;
}
.ant-tabs-nav-wrap {
padding: 0 20px;
}
.ant-tabs-nav {
margin-bottom: 0;
}
}
}
.mainTip {
@@ -50,8 +60,9 @@
padding: 0 !important;
}
.ant-tabs-content-holder {
overflow: scroll;
height: calc(100vh - 192px);
margin-top: 20px;
// overflow: scroll;
// height: calc(100vh - 175px);
}
}