[improvement][semantic-fe] Added field type and metric type to the metric creation options. (#655)

* [improvement][semantic-fe] Add model alias setting & Add view permission restrictions to the model permission management tab.
[improvement][semantic-fe] Add permission control to the action buttons for the main domain; apply high sensitivity filtering to the authorization of metrics/dimensions.
[improvement][semantic-fe] Optimize the editing mode in the dimension/metric/datasource components to use the modelId stored in the database for data, instead of relying on the data from the state manager.

* [improvement][semantic-fe] Add time granularity setting in the data source configuration.

* [improvement][semantic-fe] Dictionary import for dimension values supported in Q&A visibility

* [improvement][semantic-fe] Modification of data source creation prompt wording"

* [improvement][semantic-fe] metric market experience optimization

* [improvement][semantic-fe] enhance the analysis of metric trends

* [improvement][semantic-fe] optimize the presentation of metric trend permissions

* [improvement][semantic-fe] add metric trend download functionality

* [improvement][semantic-fe] fix the dimension initialization issue in metric correlation

* [improvement][semantic-fe] Fix the issue of database changes not taking effect when creating based on an SQL data source.

* [improvement][semantic-fe] Optimizing pagination logic and some CSS styles

* [improvement][semantic-fe] Fixing the API for the indicator list by changing "current" to "pageNum"

* [improvement][semantic-fe] Fixing the default value setting for the indicator list

* [improvement][semantic-fe] Adding batch operations for indicators/dimensions/models

* [improvement][semantic-fe] Replacing the single status update API for indicators/dimensions with a batch update API

* [improvement][semantic-fe] Redesigning the indicator homepage to incorporate trend charts and table functionality for indicators

* [improvement][semantic-fe] Optimizing the logic for setting dimension values and editing data sources, and adding system settings functionality

* [improvement][semantic-fe] Upgrading antd version to 5.x, extracting the batch operation button component, optimizing the interaction for system settings, and expanding the configuration generation types for list-to-select component.

* [improvement][semantic-fe] Adding the ability to filter dimensions based on whether they are tags or not.

* [improvement][semantic-fe] Adding the ability to edit relationships between models in the canvas.

* [improvement][semantic-fe] Updating the datePicker component to use dayjs instead.

* [improvement][semantic-fe] Fixing the issue with passing the model ID for dimensions in the indicator market.

* [improvement][semantic-fe] Fixing the abnormal state of the popup when creating a model.

* [improvement][semantic-fe] Adding permission logic for bulk operations in the indicator market.

* [improvement][semantic-fe] Adding the ability to download and transpose data.

* [improvement][semantic-fe] Fixing the initialization issue with the date selection component in the indicator details page when switching time granularity.

* [improvement][semantic-fe] Fixing the logic error in the dimension value setting.

* [improvement][semantic-fe] Fixing the synchronization issue with the question and answer settings information.

* [improvement][semantic-fe] Optimizing the canvas functionality for better performance and user experience.

* [improvement][semantic-fe] Optimizing the update process for drawing model relationship edges in the canvas.

* [improvement][semantic-fe] Changing the line type for canvas connections.

* [improvement][semantic-fe] Replacing the initialization variable from "semantic" to "headless".

* [improvement][semantic-fe] Fixing the missing migration issue for default drill-down dimension configuration in model editing. Additionally, optimizing the data retrieval method for initializing fields in the model.

* [improvement][semantic-fe] Updating the logic for the fieldName.

* [improvement][semantic-fe] Adjusting the position of the metrics tab.

* [improvement][semantic-fe] Changing the 字段名称 to 英文名称.

* [improvement][semantic-fe] Fix metric measurement deletion.

* [improvement][semantic-fe] UI optimization for metric details page.

* [improvement][semantic-fe] UI optimization for metric details page.

* [improvement][semantic-fe] UI adjustment for metric details page.

* [improvement][semantic-fe] The granularity field in the time type of model editing now supports setting it as empty.

* [improvement][semantic-fe] Added field type and metric type to the metric creation options.
This commit is contained in:
tristanliu
2024-01-19 14:46:41 +08:00
committed by GitHub
parent 0abbd83f51
commit 7af5afc3eb
24 changed files with 625 additions and 393 deletions

View File

@@ -49,4 +49,21 @@ const settings = {
'btn-disable-bg': 'rgba(0,10,36,0.04)', 'btn-disable-bg': 'rgba(0,10,36,0.04)',
}; };
export const configProviderTheme = {
components: {
Button: {
colorPrimary: '#3182ce',
},
Radio: {
colorPrimary: '#3182ce',
},
Table: {
headerBg: '#f9fafb',
headerColor: '#667085',
headerLineHeight: '38px',
headerSplitColor: '#f9fafb',
},
},
};
export default settings; export default settings;

View File

@@ -14,6 +14,7 @@ import { Copilot } from 'supersonic-chat-sdk';
import { getSystemConfig } from '@/services/user'; import { getSystemConfig } from '@/services/user';
export { request } from './services/request'; export { request } from './services/request';
import { ROUTE_AUTH_CODES } from '../config/routes'; import { ROUTE_AUTH_CODES } from '../config/routes';
import { configProviderTheme } from '../config/themeSettings';
const replaceRoute = '/'; const replaceRoute = '/';
@@ -143,15 +144,7 @@ export const layout: RunTimeLayoutConfig = (params) => {
menuHeaderRender: undefined, menuHeaderRender: undefined,
childrenRender: (dom: any) => { childrenRender: (dom: any) => {
return ( return (
<ConfigProvider <ConfigProvider theme={configProviderTheme}>
theme={{
components: {
Button: {
colorPrimary: '#3182ce',
},
},
}}
>
<div <div
style={{ height: location.pathname.includes('chat') ? 'calc(100vh - 56px)' : undefined }} style={{ height: location.pathname.includes('chat') ? 'calc(100vh - 56px)' : undefined }}
> >

View File

@@ -17,16 +17,12 @@ const FormItemTitle: React.FC<IProps> = ({
onSubTitleChange, onSubTitleChange,
}) => { }) => {
return ( return (
// <div style={{ display: 'block' }}>
// </div>
<Space direction="vertical" size={2} style={{ width: '100%' }}> <Space direction="vertical" size={2} style={{ width: '100%' }}>
<div>{title}</div> <div>{title}</div>
<div className={styles.subTitleContainer}> <div className={styles.subTitleContainer}>
{subTitleEditable ? ( {subTitleEditable ? (
<Paragraph <Paragraph
editable={{ editable={{
// editing: true,
onChange: (title: string) => { onChange: (title: string) => {
onSubTitleChange?.(title); onSubTitleChange?.(title);
}, },

View File

@@ -4,7 +4,6 @@ import { useModel } from 'umi';
import HeaderDropdown from '../HeaderDropdown'; import HeaderDropdown from '../HeaderDropdown';
import styles from './index.less'; import styles from './index.less';
import TMEAvatar from '../TMEAvatar'; import TMEAvatar from '../TMEAvatar';
import cx from 'classnames';
import { AUTH_TOKEN_KEY } from '@/common/constants'; import { AUTH_TOKEN_KEY } from '@/common/constants';
import { history } from 'umi'; import { history } from 'umi';
@@ -27,9 +26,7 @@ const { APP_TARGET } = process.env;
const AvatarDropdown: React.FC<GlobalHeaderRightProps> = () => { const AvatarDropdown: React.FC<GlobalHeaderRightProps> = () => {
const { initialState = {}, setInitialState } = useModel('@@initialState'); const { initialState = {}, setInitialState } = useModel('@@initialState');
const { currentUser = {} } = initialState as any; const { currentUser = {} } = initialState as any;
const items = [ const items = [
{ {
label: ( label: (

View File

@@ -9,7 +9,7 @@ import { updateModel, createModel, getColumns } from '../../service';
import type { Dispatch } from 'umi'; import type { Dispatch } from 'umi';
import type { StateType } from '../../model'; import type { StateType } from '../../model';
import { connect } from 'umi'; import { connect } from 'umi';
import { IDataSource, ISemantic } from '../../data'; import { ISemantic, IDataSource } from '../../data';
export type CreateFormProps = { export type CreateFormProps = {
domainManger: StateType; domainManger: StateType;
@@ -34,7 +34,7 @@ const initFormVal = {
description: '', // 模型描述 description: '', // 模型描述
}; };
const ModelCreateForm: React.FC<CreateFormProps> = ({ const DataSourceCreateForm: React.FC<CreateFormProps> = ({
domainManger, domainManger,
onCancel, onCancel,
createModalVisible, createModalVisible,
@@ -72,7 +72,9 @@ const ModelCreateForm: React.FC<CreateFormProps> = ({
setHasEmptyNameField(hasEmpty); setHasEmptyNameField(hasEmpty);
}, [fields]); }, [fields]);
const [fieldColumns, setFieldColumns] = useState(scriptColumns || []); const [fieldColumns, setFieldColumns] = useState<IDataSource.IExecuteSqlColumn[]>(
scriptColumns || [],
);
useEffect(() => { useEffect(() => {
if (scriptColumns) { if (scriptColumns) {
setFieldColumns(scriptColumns); setFieldColumns(scriptColumns);
@@ -162,6 +164,12 @@ const ModelCreateForm: React.FC<CreateFormProps> = ({
...formValRef.current, ...formValRef.current,
...fieldsValue, ...fieldsValue,
...fieldsClassify, ...fieldsClassify,
fields: fieldColumns.map((item) => {
return {
fieldName: item.nameEn,
dataType: item.type,
};
}),
}; };
updateFormVal(submitForm); updateFormVal(submitForm);
if (!saveState && currentStep < 1) { if (!saveState && currentStep < 1) {
@@ -198,25 +206,22 @@ const ModelCreateForm: React.FC<CreateFormProps> = ({
} }
}; };
const initFields = (fieldsClassifyList: any[], columns: any[]) => { const initFields = (fieldsClassifyList: any[], columns: IDataSource.IExecuteSqlColumn[]) => {
if (Array.isArray(columns) && columns.length === 0) { if (Array.isArray(columns) && columns.length === 0) {
setFields(fieldsClassifyList || []); setFields(fieldsClassifyList || []);
return; return;
} }
const columnFields: any[] = columns.map((item: any) => { const columnFields: any[] = columns.map((item: IDataSource.IExecuteSqlColumn) => {
const { type, nameEn } = item; const { type, nameEn } = item;
const oldItem = const oldItem =
fieldsClassifyList.find((oItem) => { fieldsClassifyList.find((oItem) => {
// if (oItem.type === EnumDataSourceType.MEASURES) {
// return oItem.expr === item.nameEn;
// }
return oItem.fieldName === item.nameEn; return oItem.fieldName === item.nameEn;
}) || {}; }) || {};
return { return {
...oldItem, ...oldItem,
bizName: nameEn, bizName: nameEn,
fieldName: nameEn, fieldName: nameEn,
sqlType: type, dataType: type,
}; };
}); });
setFields(columnFields || []); setFields(columnFields || []);
@@ -485,4 +490,4 @@ const ModelCreateForm: React.FC<CreateFormProps> = ({
export default connect(({ domainManger }: { domainManger: StateType }) => ({ export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger, domainManger,
}))(ModelCreateForm); }))(DataSourceCreateForm);

View File

@@ -16,7 +16,7 @@ import styles from '../style.less';
type FieldItem = { type FieldItem = {
expr?: string; expr?: string;
bizName: string; bizName: string;
sqlType: string; dataType: string;
name: string; name: string;
type: EnumDataSourceType; type: EnumDataSourceType;
agg?: string; agg?: string;
@@ -47,7 +47,7 @@ const getCreateFieldName = (type: EnumDataSourceType) => {
return isCreateName; return isCreateName;
}; };
const FieldForm: React.FC<Props> = ({ fields, sql, onFieldChange, onSqlChange }) => { const DataSourceFieldForm: React.FC<Props> = ({ fields, sql, onFieldChange, onSqlChange }) => {
const handleFieldChange = (record: FieldItem, fieldName: string, value: any) => { const handleFieldChange = (record: FieldItem, fieldName: string, value: any) => {
onFieldChange(record.bizName, { onFieldChange(record.bizName, {
...record, ...record,
@@ -61,13 +61,13 @@ const FieldForm: React.FC<Props> = ({ fields, sql, onFieldChange, onSqlChange })
dataIndex: 'fieldName', dataIndex: 'fieldName',
width: 100, width: 100,
}, },
// {
// title: '数据类型',
// dataIndex: 'sqlType',
// width: 80,
// },
{ {
title: '字段类型', title: '字段类型',
dataIndex: 'dataType',
width: 80,
},
{
title: '语义类型',
dataIndex: 'type', dataIndex: 'type',
width: 100, width: 100,
render: (_: any, record: FieldItem) => { render: (_: any, record: FieldItem) => {
@@ -305,9 +305,6 @@ const FieldForm: React.FC<Props> = ({ fields, sql, onFieldChange, onSqlChange })
<div> <div>
// //
</div> </div>
// <Marquee pauseOnHover gradient={false}>
// 为了保障同一个主题域下维度/指标列表唯一,消除歧义,若本主题域下的多个数据源存在相同的字段名并且都勾选了快速创建,系统默认这些相同字段的指标维度是同一个,同时列表中将只显示最后一次创建的指标/维度。
// </Marquee>
} }
/> />
<Table<FieldItem> <Table<FieldItem>
@@ -329,4 +326,4 @@ const FieldForm: React.FC<Props> = ({ fields, sql, onFieldChange, onSqlChange })
); );
}; };
export default FieldForm; export default DataSourceFieldForm;

View File

@@ -22,6 +22,7 @@ import styles from './style.less';
import { ISemantic } from '../data'; import { ISemantic } from '../data';
import BatchCtrlDropDownButton from '@/components/BatchCtrlDropDownButton'; import BatchCtrlDropDownButton from '@/components/BatchCtrlDropDownButton';
import MetricStar from './components/MetricStar'; import MetricStar from './components/MetricStar';
import { ColumnsConfig } from '../components/MetricTableColumnRender';
type Props = { type Props = {
dispatch: Dispatch; dispatch: Dispatch;
@@ -166,22 +167,8 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
}, },
{ {
dataIndex: 'name', dataIndex: 'name',
title: '指标名称', title: '指标',
render: (_, record: any) => { render: ColumnsConfig.metricInfo.render,
const { id, isCollect } = record;
return (
<Space>
<MetricStar metricId={id} initState={isCollect} />
<a
onClick={() => {
history.push(`/metric/detail/${record.id}`);
}}
>
{record.name}
</a>
</Space>
);
},
}, },
{ {
dataIndex: 'modelName', dataIndex: 'modelName',
@@ -203,54 +190,12 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
return <> {record.modelName}</>; return <> {record.modelName}</>;
}, },
}, },
{
dataIndex: 'sensitiveLevel',
title: '敏感度',
valueEnum: SENSITIVE_LEVEL_ENUM,
},
{ {
dataIndex: 'status', dataIndex: 'status',
title: '状态', title: '状态',
width: 80, width: 80,
search: false, search: false,
render: (status) => { render: ColumnsConfig.state.render,
switch (status) {
case StatusEnum.ONLINE:
return <Tag color="success"></Tag>;
case StatusEnum.OFFLINE:
return <Tag color="warning"></Tag>;
case StatusEnum.INITIALIZED:
return <Tag color="processing"></Tag>;
case StatusEnum.DELETED:
return <Tag color="default"></Tag>;
default:
return <Tag color="default"></Tag>;
}
},
},
{
dataIndex: 'createdBy',
title: '创建人',
search: false,
},
{
dataIndex: 'tags',
title: '标签',
search: false,
render: (tags) => {
if (Array.isArray(tags)) {
return (
<Space size={2}>
{tags.map((tag) => (
<Tag color="blue" key={tag}>
{tag}
</Tag>
))}
</Space>
);
}
return <>--</>;
},
}, },
{ {
dataIndex: 'description', dataIndex: 'description',
@@ -403,6 +348,7 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
dataSource={dataSource} dataSource={dataSource}
columns={columns} columns={columns}
pagination={pagination} pagination={pagination}
size="large"
tableAlertRender={() => { tableAlertRender={() => {
return false; return false;
}} }}
@@ -435,7 +381,6 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
setPagination(pagin); setPagination(pagin);
queryMetricList({ ...pagin, ...filterParams }); queryMetricList({ ...pagin, ...filterParams });
}} }}
size="small"
options={{ reload: false, density: false, fullScreen: false }} options={{ reload: false, density: false, fullScreen: false }}
/> />
)} )}

View File

@@ -74,7 +74,7 @@ const BindMeasuresTable: React.FC<CreateFormProps> = ({
<> <>
<Button onClick={onCancel}></Button> <Button onClick={onCancel}></Button>
<Button type="primary" onClick={handleSubmit}> <Button type="primary" onClick={handleSubmit}>
</Button> </Button>
</> </>
); );

View File

@@ -40,7 +40,7 @@ const ClassDataSourceTypeModal: React.FC<Props> = ({
const [createDataSourceModalOpen, setCreateDataSourceModalOpen] = useState<boolean>(false); const [createDataSourceModalOpen, setCreateDataSourceModalOpen] = useState<boolean>(false);
const [dataSourceEditOpen, setDataSourceEditOpen] = useState<boolean>(false); const [dataSourceEditOpen, setDataSourceEditOpen] = useState<boolean>(false);
const [currentDatabaseId, setCurrentDatabaseId] = useState<number>(); const [currentDatabaseId, setCurrentDatabaseId] = useState<number>();
const [scriptColumns, setScriptColumns] = useState<any[]>([]); const [scriptColumns, setScriptColumns] = useState<IDataSource.IExecuteSqlColumn[]>([]);
useEffect(() => { useEffect(() => {
if (!dataSourceItem?.id || !open) { if (!dataSourceItem?.id || !open) {
@@ -74,24 +74,36 @@ const ClassDataSourceTypeModal: React.FC<Props> = ({
useEffect(() => { useEffect(() => {
// queryTableColumnListByScript(dataSourceItem); // queryTableColumnListByScript(dataSourceItem);
setSql(dataSourceItem?.modelDetail?.sqlQuery); setSql(dataSourceItem?.modelDetail?.sqlQuery);
const modelDetailFields = dataSourceItem?.modelDetail?.fields;
if (Array.isArray(modelDetailFields)) {
setScriptColumns(
modelDetailFields.map((item) => {
return {
nameEn: item.fieldName,
type: item.dataType,
};
}),
);
}
}, [dataSourceItem]); }, [dataSourceItem]);
const fetchTaskResult = (params) => { // const fetchTaskResult = (params) => {
setScriptColumns(params.columns); // setScriptColumns(params.columns);
}; // };
const queryTableColumnListByScript = async (dataSource: IDataSource.IDataSourceItem) => { // const queryTableColumnListByScript = async (dataSource: IDataSource.IDataSourceItem) => {
if (!dataSource?.modelDetail?.sqlQuery) { // if (!dataSource?.modelDetail?.sqlQuery) {
return; // return;
} // }
const { code, data } = await excuteSql({ // const { code, data } = await excuteSql({
sql: dataSource.modelDetail?.sqlQuery, // sql: dataSource.modelDetail?.sqlQuery,
id: dataSource.databaseId, // id: dataSource.databaseId,
}); // });
if (code === 200) { // if (code === 200) {
fetchTaskResult(data); // fetchTaskResult(data);
} // }
}; // };
return ( return (
<> <>

View File

@@ -38,9 +38,9 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
>([]); >([]);
const [dimensionValueSettingModalVisible, setDimensionValueSettingModalVisible] = const [dimensionValueSettingModalVisible, setDimensionValueSettingModalVisible] =
useState<boolean>(false); useState<boolean>(false);
const [pagination, setPagination] = useState({ const [pagination] = useState({
current: 1, current: 1,
pageSize: 20, pageSize: 99999,
total: 0, total: 0,
}); });
@@ -54,16 +54,9 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
modelId, modelId,
}); });
setLoading(false); setLoading(false);
const { list, pageSize, pageNum, total } = data || {}; const { list } = data || {};
let resData: any = {}; let resData: any = {};
if (code === 200) { if (code === 200) {
setPagination({
...pagination,
pageSize: Math.min(pageSize, 100),
current: pageNum,
total,
});
resData = { resData = {
data: list || [], data: list || [],
success: true, success: true,
@@ -327,30 +320,6 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
}, },
}; };
// const dropdownButtonItems = [
// {
// key: 'batchStart',
// label: '批量启用',
// },
// {
// key: 'batchStop',
// label: '批量停用',
// },
// {
// key: 'batchDelete',
// label: (
// <Popconfirm
// title="确定批量删除吗?"
// onConfirm={() => {
// queryBatchUpdateStatus(selectedRowKeys, StatusEnum.DELETED);
// }}
// >
// <a>批量删除</a>
// </Popconfirm>
// ),
// },
// ];
const onMenuClick = (key: string) => { const onMenuClick = (key: string) => {
switch (key) { switch (key) {
case 'batchStart': case 'batchStart':
@@ -372,10 +341,8 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
rowKey="id" rowKey="id"
columns={columns} columns={columns}
request={queryDimensionList} request={queryDimensionList}
pagination={pagination}
loading={loading} loading={loading}
search={{ search={{
span: 4,
defaultCollapsed: false, defaultCollapsed: false,
collapseRender: () => { collapseRender: () => {
return <></>; return <></>;
@@ -385,14 +352,6 @@ const ClassDimensionTable: React.FC<Props> = ({ domainManger, dispatch }) => {
type: 'checkbox', type: 'checkbox',
...rowSelection, ...rowSelection,
}} }}
onChange={(data: any) => {
const { current, pageSize, total } = data;
setPagination({
current,
pageSize,
total,
});
}}
tableAlertRender={() => { tableAlertRender={() => {
return false; return false;
}} }}

View File

@@ -1,6 +1,6 @@
import type { ActionType, ProColumns } from '@ant-design/pro-table'; import type { ActionType, ProColumns } from '@ant-design/pro-table';
import ProTable from '@ant-design/pro-table'; import ProTable from '@ant-design/pro-table';
import { message, Button, Space, Popconfirm, Input, Tag } from 'antd'; import { message, Button, Space, Popconfirm, Input } from 'antd';
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import type { Dispatch } from 'umi'; import type { Dispatch } from 'umi';
import { StatusEnum } from '../enum'; import { StatusEnum } from '../enum';
@@ -19,6 +19,7 @@ import BatchCtrlDropDownButton from '@/components/BatchCtrlDropDownButton';
import moment from 'moment'; import moment from 'moment';
import styles from './style.less'; import styles from './style.less';
import { ISemantic } from '../data'; import { ISemantic } from '../data';
import { ColumnsConfig } from './MetricTableColumnRender';
type Props = { type Props = {
dispatch: Dispatch; dispatch: Dispatch;
@@ -92,16 +93,12 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
}; };
const columns: ProColumns[] = [ const columns: ProColumns[] = [
{
dataIndex: 'id',
title: 'ID',
width: 80,
search: false,
},
{ {
dataIndex: 'name', dataIndex: 'name',
title: '指标名称', title: '指标',
width: '30%',
search: false, search: false,
render: ColumnsConfig.metricInfo.render,
}, },
{ {
dataIndex: 'key', dataIndex: 'key',
@@ -109,74 +106,25 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
hideInTable: true, hideInTable: true,
renderFormItem: () => <Input placeholder="请输入ID/指标名称/英文名称/标签" />, renderFormItem: () => <Input placeholder="请输入ID/指标名称/英文名称/标签" />,
}, },
{
dataIndex: 'alias',
title: '别名',
width: 150,
ellipsis: true,
search: false,
},
{
dataIndex: 'bizName',
title: '英文名称',
search: false,
},
{ {
dataIndex: 'sensitiveLevel', dataIndex: 'sensitiveLevel',
title: '敏感度', title: '敏感度',
width: 80, hideInTable: true,
valueEnum: SENSITIVE_LEVEL_ENUM, valueEnum: SENSITIVE_LEVEL_ENUM,
}, },
{
dataIndex: 'status',
title: '状态',
width: 80,
search: false,
render: (status) => {
switch (status) {
case StatusEnum.ONLINE:
return <Tag color="success"></Tag>;
case StatusEnum.OFFLINE:
return <Tag color="warning"></Tag>;
case StatusEnum.INITIALIZED:
return <Tag color="processing"></Tag>;
case StatusEnum.DELETED:
return <Tag color="default"></Tag>;
default:
return <Tag color="default"></Tag>;
}
},
},
{
dataIndex: 'createdBy',
title: '创建人',
width: 100,
search: false,
},
{
dataIndex: 'tags',
title: '标签',
search: false,
render: (tags) => {
if (Array.isArray(tags)) {
return (
<Space size={2} wrap>
{tags.map((tag) => (
<Tag color="blue" key={tag}>
{tag}
</Tag>
))}
</Space>
);
}
return <>--</>;
},
},
{ {
dataIndex: 'description', dataIndex: 'description',
title: '描述', title: '描述',
search: false, search: false,
}, },
{
dataIndex: 'status',
title: '状态',
width: 200,
search: false,
render: ColumnsConfig.state.render,
},
{ {
dataIndex: 'updatedAt', dataIndex: 'updatedAt',
title: '更新时间', title: '更新时间',
@@ -302,11 +250,8 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
actionRef={actionRef} actionRef={actionRef}
rowKey="id" rowKey="id"
search={{ search={{
span: 4, optionRender: false,
defaultCollapsed: false, collapsed: false,
collapseRender: () => {
return <></>;
},
}} }}
rowSelection={{ rowSelection={{
type: 'checkbox', type: 'checkbox',
@@ -327,7 +272,7 @@ const ClassMetricTable: React.FC<Props> = ({ domainManger, dispatch }) => {
total, total,
}); });
}} }}
size="small" size="large"
options={{ reload: false, density: false, fullScreen: false }} options={{ reload: false, density: false, fullScreen: false }}
toolBarRender={() => [ toolBarRender={() => [
<Button <Button

View File

@@ -1,15 +1,22 @@
import { useEffect, forwardRef, useImperativeHandle, useState } from 'react'; import { useEffect, forwardRef, useImperativeHandle, useState } from 'react';
import type { ForwardRefRenderFunction } from 'react'; import type { ForwardRefRenderFunction } from 'react';
import { message, Form, Input, Select, Button, Space } from 'antd'; import { message, Form, Input, Select, Button, Space } from 'antd';
import { saveDatabase, testDatabaseConnect } from '../../service'; import {
saveDatabase,
testDatabaseConnect,
getDatabaseParameters,
getDatabaseDetail,
} from '../../service';
import { formLayout } from '@/components/FormHelper/utils'; import { formLayout } from '@/components/FormHelper/utils';
import SelectTMEPerson from '@/components/SelectTMEPerson'; import SelectTMEPerson from '@/components/SelectTMEPerson';
import { ConfigParametersItem } from '../../../System/types';
import { genneratorFormItemList } from '../../utils';
import { ISemantic } from '../../data'; import { ISemantic } from '../../data';
import styles from '../style.less'; import styles from '../style.less';
type Props = { type Props = {
domainId?: number; domainId?: number;
dataBaseConfig?: ISemantic.IDatabaseItem; databaseId?: number;
hideSubmitBtn?: boolean; hideSubmitBtn?: boolean;
onSubmit?: (params?: any) => void; onSubmit?: (params?: any) => void;
}; };
@@ -18,21 +25,59 @@ const FormItem = Form.Item;
const TextArea = Input.TextArea; const TextArea = Input.TextArea;
const DatabaseCreateForm: ForwardRefRenderFunction<any, Props> = ( const DatabaseCreateForm: ForwardRefRenderFunction<any, Props> = (
{ domainId, dataBaseConfig, onSubmit, hideSubmitBtn = false }, { domainId, databaseId, onSubmit, hideSubmitBtn = false },
ref, ref,
) => { ) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [selectedDbType, setSelectedDbType] = useState<string>('h2'); const [selectedDbType, setSelectedDbType] = useState<string>('');
const [databaseOptions, setDatabaseOptions] = useState<{ value: string; label: string }[]>([]);
const [databaseConfig, setDatabaseConfig] = useState<Record<string, ConfigParametersItem[]>>({});
const [testLoading, setTestLoading] = useState<boolean>(false); const [testLoading, setTestLoading] = useState<boolean>(false);
const [dataBaseDetail, setDataBaseDetail] = useState<ISemantic.IDatabaseItem>();
useEffect(() => { useEffect(() => {
form.resetFields(); form.resetFields();
if (dataBaseConfig) { if (dataBaseDetail) {
form.setFieldsValue({ ...dataBaseConfig }); form.setFieldsValue({ ...dataBaseDetail });
setSelectedDbType(dataBaseConfig?.type); setSelectedDbType(dataBaseDetail?.type);
} }
}, [dataBaseConfig]); }, [dataBaseDetail]);
useEffect(() => {
if (databaseId) {
queryDatabaseDetail(databaseId);
}
}, [databaseId]);
useEffect(() => {
queryDatabaseConfig();
}, []);
const queryDatabaseDetail = async (id: number) => {
const { code, msg, data } = await getDatabaseDetail(id);
if (code === 200) {
setDataBaseDetail(data);
return;
}
message.error(msg);
};
const queryDatabaseConfig = async () => {
const { code, msg, data } = await getDatabaseParameters();
if (code === 200) {
const options = Object.keys(data).map((sqlName: string) => {
return {
value: sqlName,
label: sqlName,
};
});
setDatabaseConfig(data);
setDatabaseOptions(options);
return;
}
message.error(msg);
};
const getFormValidateFields = async () => { const getFormValidateFields = async () => {
return await form.validateFields(); return await form.validateFields();
@@ -47,7 +92,7 @@ const DatabaseCreateForm: ForwardRefRenderFunction<any, Props> = (
const saveDatabaseConfig = async () => { const saveDatabaseConfig = async () => {
const values = await form.validateFields(); const values = await form.validateFields();
const { code, msg } = await saveDatabase({ const { code, msg } = await saveDatabase({
...dataBaseConfig, ...(dataBaseDetail || {}),
...values, ...values,
domainId, domainId,
}); });
@@ -98,14 +143,12 @@ const DatabaseCreateForm: ForwardRefRenderFunction<any, Props> = (
<Select <Select
style={{ width: '100%' }} style={{ width: '100%' }}
placeholder="请选择数据库类型" placeholder="请选择数据库类型"
options={[ options={databaseOptions}
{ value: 'h2', label: 'h2' },
{ value: 'mysql', label: 'mysql' },
{ value: 'clickhouse', label: 'clickhouse' },
]}
/> />
</FormItem> </FormItem>
{selectedDbType === 'h2' ? (
{databaseConfig[selectedDbType] && genneratorFormItemList(databaseConfig[selectedDbType])}
{/* {selectedDbType === 'h2' ? (
<FormItem name="url" label="链接" rules={[{ required: true, message: '请输入链接' }]}> <FormItem name="url" label="链接" rules={[{ required: true, message: '请输入链接' }]}>
<Input placeholder="请输入链接" /> <Input placeholder="请输入链接" />
</FormItem> </FormItem>
@@ -139,11 +182,10 @@ const DatabaseCreateForm: ForwardRefRenderFunction<any, Props> = (
]} ]}
/> />
</FormItem> </FormItem>
)} )} */}
<FormItem {/* <FormItem
name="username" name="username"
label="用户名" label="用户名"
// rules={[{ required: true, message: '请输入用户名' }]}
> >
<Input placeholder="请输入用户名" /> <Input placeholder="请输入用户名" />
</FormItem> </FormItem>
@@ -152,7 +194,7 @@ const DatabaseCreateForm: ForwardRefRenderFunction<any, Props> = (
</FormItem> </FormItem>
<FormItem name="database" label="数据库名称"> <FormItem name="database" label="数据库名称">
<Input placeholder="请输入数据库名称" /> <Input placeholder="请输入数据库名称" />
</FormItem> </FormItem> */}
<FormItem <FormItem
name="admins" name="admins"
label="管理员" label="管理员"

View File

@@ -55,7 +55,7 @@ const DatabaseSettingModal: React.FC<CreateFormProps> = ({
return ( return (
<Modal <Modal
width={1200} width={600}
destroyOnClose destroyOnClose
title="数据库连接设置" title="数据库连接设置"
style={{ top: 48 }} style={{ top: 48 }}
@@ -67,7 +67,8 @@ const DatabaseSettingModal: React.FC<CreateFormProps> = ({
<DatabaseCreateForm <DatabaseCreateForm
hideSubmitBtn={true} hideSubmitBtn={true}
ref={createFormRef} ref={createFormRef}
dataBaseConfig={databaseItem} // dataBaseConfig={databaseItem}
databaseId={databaseItem?.id}
onSubmit={() => { onSubmit={() => {
onSubmit?.(); onSubmit?.();
}} }}

View File

@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { Modal, Button, message } from 'antd'; import { Modal, Button, message } from 'antd';
import DimensionMetricRelationTableTransfer from './DimensionMetricRelationTableTransfer'; import DimensionMetricRelationTableTransfer from './DimensionMetricRelationTableTransfer';
import { ISemantic } from '../data'; import { ISemantic } from '../data';
import { updateExprMetric } from '../service'; import { updateMetric } from '../service';
import FormItemTitle from '@/components/FormHelper/FormItemTitle'; import FormItemTitle from '@/components/FormHelper/FormItemTitle';
type Props = { type Props = {
@@ -15,7 +15,7 @@ type Props = {
const DimensionAndMetricRelationModal: React.FC<Props> = ({ const DimensionAndMetricRelationModal: React.FC<Props> = ({
open, open,
metricItem = {}, metricItem,
relationsInitialValue, relationsInitialValue,
onCancel, onCancel,
onSubmit, onSubmit,
@@ -30,7 +30,7 @@ const DimensionAndMetricRelationModal: React.FC<Props> = ({
drillDownDimensions: relationList, drillDownDimensions: relationList,
}, },
}; };
const { code, msg } = await updateExprMetric(queryParams); const { code, msg } = await updateMetric(queryParams);
if (code === 200) { if (code === 200) {
onSubmit(relationList); onSubmit(relationList);
return; return;
@@ -45,7 +45,11 @@ const DimensionAndMetricRelationModal: React.FC<Props> = ({
<Button <Button
type="primary" type="primary"
onClick={() => { onClick={() => {
if (metricItem?.id) {
saveMetric(relationList); saveMetric(relationList);
} else {
onSubmit(relationList);
}
}} }}
> >

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { Space } from 'antd';
type Props = { type Props = {
title: string; title: string;
@@ -15,7 +16,12 @@ const FormLabelRequire: React.FC<Props> = ({ title, labelStyles = {} }) => {
title={title} title={title}
style={{ fontSize: '16px', ...labelStyles }} style={{ fontSize: '16px', ...labelStyles }}
> >
<Space size={5}>
<span style={{ color: '#ff4d4f', fontSize: '18px', position: 'relative', top: 3 }}>
*
</span>
{title} {title}
</Space>
</label> </label>
</div> </div>
</> </>

View File

@@ -15,19 +15,23 @@ import {
Col, Col,
Space, Space,
Tooltip, Tooltip,
Tag,
} from 'antd'; } from 'antd';
import { InfoCircleOutlined } from '@ant-design/icons'; import { InfoCircleOutlined } from '@ant-design/icons';
import MetricMeasuresFormTable from './MetricMeasuresFormTable'; import MetricMeasuresFormTable from './MetricMeasuresFormTable';
import { SENSITIVE_LEVEL_OPTIONS } from '../constant'; import { SENSITIVE_LEVEL_OPTIONS, METRIC_DEFINE_TYPE } from '../constant';
import { formLayout } from '@/components/FormHelper/utils'; import { formLayout } from '@/components/FormHelper/utils';
import FormItemTitle from '@/components/FormHelper/FormItemTitle'; import FormItemTitle from '@/components/FormHelper/FormItemTitle';
import styles from './style.less'; import styles from './style.less';
import { getMeasureListByModelId, getModelDetail } from '../service'; import { getMetricsToCreateNewMetric, getModelDetail } from '../service';
import MetricMetricFormTable from './MetricMetricFormTable';
import MetricFieldFormTable from './MetricFieldFormTable';
import DimensionAndMetricRelationModal from './DimensionAndMetricRelationModal'; import DimensionAndMetricRelationModal from './DimensionAndMetricRelationModal';
import TableTitleTooltips from '../components/TableTitleTooltips'; import TableTitleTooltips from '../components/TableTitleTooltips';
import { creatExprMetric, updateExprMetric, mockMetricAlias, getMetricTags } from '../service'; import { createMetric, updateMetric, mockMetricAlias, getMetricTags } from '../service';
import { ISemantic } from '../data'; import { ISemantic } from '../data';
import { history } from 'umi'; import { history } from 'umi';
import { cloneDeep } from 'lodash';
export type CreateFormProps = { export type CreateFormProps = {
datasourceId?: number; datasourceId?: number;
@@ -44,6 +48,12 @@ const FormItem = Form.Item;
const { TextArea } = Input; const { TextArea } = Input;
const { Option } = Select; const { Option } = Select;
const queryParamsTypeParamsKey = {
[METRIC_DEFINE_TYPE.MEASURE]: 'metricDefineByMeasureParams',
[METRIC_DEFINE_TYPE.METRIC]: 'metricDefineByMetricParams',
[METRIC_DEFINE_TYPE.FIELD]: 'metricDefineByFieldParams',
};
const MetricInfoCreateForm: React.FC<CreateFormProps> = ({ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
datasourceId, datasourceId,
domainId, domainId,
@@ -67,10 +77,31 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
const [classMeasureList, setClassMeasureList] = useState<ISemantic.IMeasure[]>([]); const [classMeasureList, setClassMeasureList] = useState<ISemantic.IMeasure[]>([]);
const [exprTypeParamsState, setExprTypeParamsState] = useState<ISemantic.IMeasure[]>([]); const [exprTypeParamsState, setExprTypeParamsState] = useState<{
[METRIC_DEFINE_TYPE.MEASURE]: ISemantic.IMeasureTypeParams;
[METRIC_DEFINE_TYPE.METRIC]: ISemantic.IMetricTypeParams;
[METRIC_DEFINE_TYPE.FIELD]: ISemantic.IFieldTypeParams;
}>({
[METRIC_DEFINE_TYPE.MEASURE]: {
measures: [],
expr: '',
},
[METRIC_DEFINE_TYPE.METRIC]: {
metrics: [],
expr: '',
},
[METRIC_DEFINE_TYPE.FIELD]: {
fields: [],
expr: '',
},
} as any);
const [exprSql, setExprSql] = useState<string>(''); // const [exprTypeParamsState, setExprTypeParamsState] = useState<ISemantic.IMeasure[]>([]);
const [defineType, setDefineType] = useState(METRIC_DEFINE_TYPE.MEASURE);
const [createNewMetricList, setCreateNewMetricList] = useState<ISemantic.IMetricItem[]>([]);
const [fieldList, setFieldList] = useState<string[]>([]);
const [isPercentState, setIsPercentState] = useState<boolean>(false); const [isPercentState, setIsPercentState] = useState<boolean>(false);
const [isDecimalState, setIsDecimalState] = useState<boolean>(false); const [isDecimalState, setIsDecimalState] = useState<boolean>(false);
const [hasMeasuresState, setHasMeasuresState] = useState<boolean>(true); const [hasMeasuresState, setHasMeasuresState] = useState<boolean>(true);
@@ -87,10 +118,13 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
const forward = () => setCurrentStep(currentStep + 1); const forward = () => setCurrentStep(currentStep + 1);
const backward = () => setCurrentStep(currentStep - 1); const backward = () => setCurrentStep(currentStep - 1);
const queryClassMeasureList = async () => { const queryModelDetail = async () => {
// const { code, data } = await getMeasureListByModelId(modelId); // const { code, data } = await getMeasureListByModelId(modelId);
const { code, data } = await getModelDetail({ modelId: modelId || metricItem?.modelId }); const { code, data } = await getModelDetail({ modelId: modelId || metricItem?.modelId });
if (code === 200) { if (code === 200) {
if (Array.isArray(data?.modelDetail?.fields)) {
setFieldList(data.modelDetail.fields);
}
if (Array.isArray(data?.modelDetail?.measures)) { if (Array.isArray(data?.modelDetail?.measures)) {
setClassMeasureList(data.modelDetail.measures); setClassMeasureList(data.modelDetail.measures);
if (datasourceId) { if (datasourceId) {
@@ -106,7 +140,8 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
}; };
useEffect(() => { useEffect(() => {
queryClassMeasureList(); queryModelDetail();
queryMetricsToCreateNewMetric();
queryMetricTags(); queryMetricTags();
}, []); }, []);
@@ -115,10 +150,8 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
const submitForm = { const submitForm = {
...formValRef.current, ...formValRef.current,
...fieldsValue, ...fieldsValue,
typeParams: { metricDefineType: defineType,
expr: exprSql, [queryParamsTypeParamsKey[defineType]]: exprTypeParamsState[defineType],
measures: exprTypeParamsState,
},
}; };
updateFormVal(submitForm); updateFormVal(submitForm);
if (currentStep < 1) { if (currentStep < 1) {
@@ -135,12 +168,16 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
bizName, bizName,
description, description,
sensitiveLevel, sensitiveLevel,
typeParams: typeParams, typeParams,
dataFormat, dataFormat,
dataFormatType, dataFormatType,
alias, alias,
tags, tags,
} = metricItem as any; metricDefineType,
metricDefineByMeasureParams,
metricDefineByMetricParams,
metricDefineByFieldParams,
} = metricItem;
const isPercent = dataFormatType === 'percent'; const isPercent = dataFormatType === 'percent';
const isDecimal = dataFormatType === 'decimal'; const isDecimal = dataFormatType === 'decimal';
const initValue = { const initValue = {
@@ -162,10 +199,39 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
...formValRef.current, ...formValRef.current,
...initValue, ...initValue,
}; };
if (metricDefineType === METRIC_DEFINE_TYPE.MEASURE) {
const { measures, expr } = metricDefineByMeasureParams || {};
setExprTypeParamsState({
...exprTypeParamsState,
[METRIC_DEFINE_TYPE.MEASURE]: {
measures: measures || [],
expr: expr || '',
},
});
}
if (metricDefineType === METRIC_DEFINE_TYPE.METRIC) {
const { metrics, expr } = metricDefineByMetricParams || {};
setExprTypeParamsState({
...exprTypeParamsState,
[METRIC_DEFINE_TYPE.METRIC]: {
metrics: metrics || [],
expr: expr || '',
},
});
}
if (metricDefineType === METRIC_DEFINE_TYPE.FIELD) {
const { fields, expr } = metricDefineByFieldParams || {};
setExprTypeParamsState({
...exprTypeParamsState,
[METRIC_DEFINE_TYPE.FIELD]: {
fields: fields || [],
expr: expr || '',
},
});
}
updateFormVal(editInitFormVal); updateFormVal(editInitFormVal);
form.setFieldsValue(initValue); form.setFieldsValue(initValue);
setExprTypeParamsState(typeParams.measures); setDefineType(metricDefineType);
setExprSql(typeParams.expr);
setIsPercentState(isPercent); setIsPercentState(isPercent);
setIsDecimalState(isDecimal); setIsDecimalState(isDecimal);
}; };
@@ -176,6 +242,37 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
} }
}, [metricItem]); }, [metricItem]);
const isEmptyConditions = (
metricDefineType: METRIC_DEFINE_TYPE,
metricDefineParams:
| ISemantic.IMeasureTypeParams
| ISemantic.IMetricTypeParams
| ISemantic.IFieldTypeParams,
) => {
if (metricDefineType === METRIC_DEFINE_TYPE.MEASURE) {
const { measures } = (metricDefineParams as ISemantic.IMeasureTypeParams) || {};
if (!(Array.isArray(measures) && measures.length > 0)) {
message.error('请添加一个度量');
return true;
}
}
if (metricDefineType === METRIC_DEFINE_TYPE.METRIC) {
const { metrics } = (metricDefineParams as ISemantic.IMetricTypeParams) || {};
if (!(Array.isArray(metrics) && metrics.length > 0)) {
message.error('请添加一个指标');
return true;
}
}
if (metricDefineType === METRIC_DEFINE_TYPE.FIELD) {
const { fields } = (metricDefineParams as ISemantic.IFieldTypeParams) || {};
if (!(Array.isArray(fields) && fields.length > 0)) {
message.error('请添加一个字段');
return true;
}
}
return false;
};
const saveMetric = async (fieldsValue: any) => { const saveMetric = async (fieldsValue: any) => {
const queryParams = { const queryParams = {
modelId: isEdit ? metricItem.modelId : modelId, modelId: isEdit ? metricItem.modelId : modelId,
@@ -185,22 +282,22 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
}, },
...fieldsValue, ...fieldsValue,
}; };
const { typeParams, alias, dataFormatType } = queryParams; const { alias, dataFormatType } = queryParams;
queryParams.alias = Array.isArray(alias) ? alias.join(',') : ''; queryParams.alias = Array.isArray(alias) ? alias.join(',') : '';
if (!typeParams?.expr) { if (!queryParams[queryParamsTypeParamsKey[defineType]]?.expr) {
message.error('请输入度量表达式'); message.error('请输入度量表达式');
return; return;
} }
if (!dataFormatType) { if (!dataFormatType) {
delete queryParams.dataFormat; delete queryParams.dataFormat;
} }
if (!(Array.isArray(typeParams?.measures) && typeParams.measures.length > 0)) { if (isEmptyConditions(defineType, queryParams[queryParamsTypeParamsKey[defineType]])) {
message.error('请添加一个度量');
return; return;
} }
let saveMetricQuery = creatExprMetric;
let saveMetricQuery = createMetric;
if (queryParams.id) { if (queryParams.id) {
saveMetricQuery = updateExprMetric; saveMetricQuery = updateMetric;
} }
const { code, msg } = await saveMetricQuery(queryParams); const { code, msg } = await saveMetricQuery(queryParams);
if (code === 200) { if (code === 200) {
@@ -238,24 +335,141 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
message.error('获取指标标签失败'); message.error('获取指标标签失败');
} }
}; };
const queryMetricsToCreateNewMetric = async () => {
const { code, data } = await getMetricsToCreateNewMetric({ modelId });
if (code === 200) {
setCreateNewMetricList(data);
} else {
message.error('获取指标标签失败');
}
};
const renderContent = () => { const renderContent = () => {
if (currentStep === 1) { if (currentStep === 1) {
return ( return (
<div>
<div
style={{
padding: '0 0 20px 24px',
// borderBottom: '1px solid #eee',
}}
>
<Radio.Group
buttonStyle="solid"
value={defineType}
onChange={(e) => {
setDefineType(e.target.value);
}}
>
<Radio.Button value={METRIC_DEFINE_TYPE.MEASURE}></Radio.Button>
<Radio.Button value={METRIC_DEFINE_TYPE.METRIC}></Radio.Button>
<Radio.Button value={METRIC_DEFINE_TYPE.FIELD}></Radio.Button>
</Radio.Group>
</div>
{defineType === METRIC_DEFINE_TYPE.MEASURE && (
<>
<MetricMeasuresFormTable <MetricMeasuresFormTable
datasourceId={datasourceId} datasourceId={datasourceId}
typeParams={{ typeParams={exprTypeParamsState[METRIC_DEFINE_TYPE.MEASURE]}
measures: exprTypeParamsState,
expr: exprSql,
}}
measuresList={classMeasureList} measuresList={classMeasureList}
onFieldChange={(typeParams: any) => { onFieldChange={(measures: ISemantic.IMeasure[]) => {
setExprTypeParamsState([...typeParams]); setExprTypeParamsState((prevState) => {
return {
...prevState,
[METRIC_DEFINE_TYPE.MEASURE]: {
...prevState[METRIC_DEFINE_TYPE.MEASURE],
measures,
},
};
});
}} }}
onSqlChange={(sql: string) => { onSqlChange={(expr: string) => {
setExprSql(sql); setExprTypeParamsState((prevState) => {
return {
...prevState,
[METRIC_DEFINE_TYPE.MEASURE]: {
...prevState[METRIC_DEFINE_TYPE.MEASURE],
expr,
},
};
});
}} }}
/> />
</>
)}
{defineType === METRIC_DEFINE_TYPE.METRIC && (
<>
<p className={styles.desc}>
<Tag color="#2499ef14" className={styles.markerTag}>
</Tag>
<Tag color="#2499ef14" className={styles.markerTag}>
</Tag>
</p>
<MetricMetricFormTable
typeParams={exprTypeParamsState[METRIC_DEFINE_TYPE.METRIC]}
metricList={createNewMetricList}
onFieldChange={(metrics: ISemantic.IMetricTypeParamsItem[]) => {
setExprTypeParamsState((prevState) => {
return {
...prevState,
[METRIC_DEFINE_TYPE.METRIC]: {
...prevState[METRIC_DEFINE_TYPE.METRIC],
metrics,
},
};
});
}}
onSqlChange={(expr: string) => {
setExprTypeParamsState((prevState) => {
return {
...prevState,
[METRIC_DEFINE_TYPE.METRIC]: {
...prevState[METRIC_DEFINE_TYPE.METRIC],
expr,
},
};
});
}}
/>
</>
)}
{defineType === METRIC_DEFINE_TYPE.FIELD && (
<>
<MetricFieldFormTable
typeParams={exprTypeParamsState[METRIC_DEFINE_TYPE.FIELD]}
fieldList={fieldList}
onFieldChange={(fields: ISemantic.IFieldTypeParamsItem[]) => {
setExprTypeParamsState((prevState) => {
return {
...prevState,
[METRIC_DEFINE_TYPE.FIELD]: {
...prevState[METRIC_DEFINE_TYPE.FIELD],
fields,
},
};
});
}}
onSqlChange={(expr: string) => {
setExprTypeParamsState((prevState) => {
return {
...prevState,
[METRIC_DEFINE_TYPE.FIELD]: {
...prevState[METRIC_DEFINE_TYPE.FIELD],
expr,
},
};
});
}}
/>
</>
)}
</div>
); );
} }
@@ -456,9 +670,9 @@ const MetricInfoCreateForm: React.FC<CreateFormProps> = ({
return ( return (
<Modal <Modal
forceRender forceRender
width={1300} width={800}
style={{ top: 48 }} style={{ top: 48 }}
styles={{ padding: '32px 40px 48px' }} // styles={{ padding: '32px 40px 48px' }}
destroyOnClose destroyOnClose
title={`${isEdit ? '编辑' : '新建'}指标`} title={`${isEdit ? '编辑' : '新建'}指标`}
maskClosable={false} maskClosable={false}

View File

@@ -1,15 +1,16 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { Button, Input, Space } from 'antd'; import { Button, Input, Space, Tag } from 'antd';
import ProTable from '@ant-design/pro-table'; import ProTable from '@ant-design/pro-table';
import ProCard from '@ant-design/pro-card'; import ProCard from '@ant-design/pro-card';
import SqlEditor from '@/components/SqlEditor'; import SqlEditor from '@/components/SqlEditor';
import BindMeasuresTable from './BindMeasuresTable'; import BindMeasuresTable from './BindMeasuresTable';
import FormLabelRequire from './FormLabelRequire'; import FormLabelRequire from './FormLabelRequire';
import styles from './style.less';
import { ISemantic } from '../data'; import { ISemantic } from '../data';
type Props = { type Props = {
datasourceId?: number; datasourceId?: number;
typeParams: ISemantic.ITypeParams; typeParams: ISemantic.IMeasureTypeParams;
measuresList: ISemantic.IMeasure[]; measuresList: ISemantic.IMeasure[];
onFieldChange: (measures: ISemantic.IMeasure[]) => void; onFieldChange: (measures: ISemantic.IMeasure[]) => void;
onSqlChange: (sql: string) => void; onSqlChange: (sql: string) => void;
@@ -25,7 +26,6 @@ const MetricMeasuresFormTable: React.FC<Props> = ({
onSqlChange, onSqlChange,
}) => { }) => {
const actionRef = useRef<ActionType>(); const actionRef = useRef<ActionType>();
const [measuresModalVisible, setMeasuresModalVisible] = useState<boolean>(false); const [measuresModalVisible, setMeasuresModalVisible] = useState<boolean>(false);
const [measuresParams, setMeasuresParams] = useState( const [measuresParams, setMeasuresParams] = useState(
typeParams || { typeParams || {
@@ -44,6 +44,7 @@ const MetricMeasuresFormTable: React.FC<Props> = ({
{ {
dataIndex: 'bizName', dataIndex: 'bizName',
title: '度量名称', title: '度量名称',
tooltip: '由模型名称_字段名称拼接而来',
}, },
{ {
dataIndex: 'constraint', dataIndex: 'constraint',
@@ -74,6 +75,11 @@ const MetricMeasuresFormTable: React.FC<Props> = ({
); );
}, },
}, },
{
dataIndex: 'agg',
title: '聚合函数',
},
{ {
title: '操作', title: '操作',
dataIndex: 'x', dataIndex: 'x',
@@ -105,7 +111,6 @@ const MetricMeasuresFormTable: React.FC<Props> = ({
<ProTable <ProTable
actionRef={actionRef} actionRef={actionRef}
headerTitle={<FormLabelRequire title="度量列表" />} headerTitle={<FormLabelRequire title="度量列表" />}
tooltip="基于本主题域下所有数据源的度量来创建指标且该列表的度量为了加以区分均已加上数据源名称作为前缀选中度量后可基于这几个度量来写表达式若是选中的度量来自不同的数据源系统将会自动join来计算该指标"
rowKey="name" rowKey="name"
columns={columns} columns={columns}
dataSource={measuresParams?.measures || []} dataSource={measuresParams?.measures || []}
@@ -127,8 +132,24 @@ const MetricMeasuresFormTable: React.FC<Props> = ({
/> />
<ProCard <ProCard
title={<FormLabelRequire title="度量表达式" />} title={<FormLabelRequire title="度量表达式" />}
tooltip="度量表达式由上面选择的度量组成如选择了度量A和B则可将表达式写成A+B" // tooltip="由于度量已自带聚合函数,因此通过度量创建指标时,表达式中无需再写聚合函数,如
// 通过度量a和度量b来创建指标由于建模的时候度量a和度量b被指定了聚合函数SUM因此创建指标时表达式只需要写成 a+b, 而不需要带聚合函数"
> >
<p
className={styles.desc}
style={{ border: 'unset', padding: 0, marginBottom: 20, marginLeft: 2 }}
>
<Tag color="#2499ef14" className={styles.markerTag}>
</Tag>
<Tag color="#2499ef14" className={styles.markerTag}>
</Tag>
:
通过指定了聚合函数SUM的度量a和度量b来创建指标 a+b
</p>
<SqlEditor <SqlEditor
value={exprString} value={exprString}
onChange={(sql: string) => { onChange={(sql: string) => {
@@ -149,11 +170,12 @@ const MetricMeasuresFormTable: React.FC<Props> = ({
} }
selectedMeasuresList={measuresParams?.measures || []} selectedMeasuresList={measuresParams?.measures || []}
onSubmit={async (values: any[]) => { onSubmit={async (values: any[]) => {
const measures = values.map(({ bizName, name, expr, datasourceId }) => { const measures = values.map(({ bizName, name, expr, datasourceId, agg }) => {
return { return {
bizName, bizName,
name, name,
expr, expr,
agg,
datasourceId, datasourceId,
}; };
}); });

View File

@@ -240,6 +240,9 @@
.ant-pro-table-search-query-filter { .ant-pro-table-search-query-filter {
margin-bottom: 0; margin-bottom: 0;
} }
.ant-pro-query-filter {
padding-bottom: 0;
}
.ant-pro-table-list-toolbar-container { .ant-pro-table-list-toolbar-container {
padding-top: 0; padding-top: 0;
} }
@@ -392,3 +395,25 @@
} }
} }
} }
.desc {
margin: 0;
padding: 25px;
color: #667085;
font-size: 14px;
border-bottom: 1px solid #eee;
border-top: 1px solid #eee;
margin-bottom: 10px;
.markerTag {
color: #2499ef;
font-size: 14px;
margin: 0 2px;
}
}
.textLink {
color: #101828;
&:hover {
color: #69b1ff;
}
}

View File

@@ -37,7 +37,7 @@ export const IS_TAG_ENUM = {
}; };
export const SENSITIVE_LEVEL_COLOR = { export const SENSITIVE_LEVEL_COLOR = {
[SENSITIVE_LEVEL.LOW]: 'lime', [SENSITIVE_LEVEL.LOW]: 'geekblue',
[SENSITIVE_LEVEL.MID]: 'warning', [SENSITIVE_LEVEL.MID]: 'warning',
[SENSITIVE_LEVEL.HIGH]: 'error', [SENSITIVE_LEVEL.HIGH]: 'error',
}; };
@@ -71,3 +71,9 @@ export const DatePeridMap = {
sys_imp_week: DateRangeType.WEEK, sys_imp_week: DateRangeType.WEEK,
sys_imp_month: DateRangeType.MONTH, sys_imp_month: DateRangeType.MONTH,
}; };
export enum METRIC_DEFINE_TYPE {
FIELD = 'FIELD',
MEASURE = 'MEASURE',
METRIC = 'METRIC',
}

View File

@@ -14,6 +14,16 @@ export type SensitiveLevel = 0 | 1 | 2 | null;
export type ToolBarSearchCallBack = (text: string) => void; export type ToolBarSearchCallBack = (text: string) => void;
export declare namespace IDataSource { export declare namespace IDataSource {
interface IExecuteSqlColumn {
name?: string;
type: string;
nameEn: string;
showType?: string;
authorized?: boolean;
dataFormatType?: string;
dataFormat?: string;
}
interface IIdentifiersItem { interface IIdentifiersItem {
name: string; name: string;
type: string; type: string;
@@ -42,12 +52,17 @@ export declare namespace IDataSource {
nameCh: string; nameCh: string;
isCreateMetric: number; isCreateMetric: number;
} }
interface IDataSourceDetailFieldsItem {
dataType: string;
fieldName: string;
}
interface IDataSourceDetail { interface IDataSourceDetail {
queryType: string; queryType: string;
sqlQuery: string; sqlQuery: string;
tableQuery: string; tableQuery: string;
identifiers: IIdentifiersItem[]; identifiers: IIdentifiersItem[];
fields: IDataSourceDetailFieldsItem[];
dimensions: IDimensionsItem[]; dimensions: IDimensionsItem[];
measures: IMeasuresItem[]; measures: IMeasuresItem[];
} }
@@ -168,11 +183,30 @@ export declare namespace ISemantic {
isCreateMetric?: number; isCreateMetric?: number;
datasourceId: number; datasourceId: number;
} }
interface ITypeParams {
interface IFieldTypeParamsItem {
fieldName: string;
}
interface IMetricTypeParamsItem {
id: number;
bizName: string;
}
interface IMeasureTypeParams {
measures: IMeasure[]; measures: IMeasure[];
expr: string; expr: string;
} }
interface IMetricTypeParams {
expr: string;
metrics: IMetricTypeParamsItem[];
}
interface IFieldTypeParams {
expr: string;
fields: IFieldTypeParamsItem[];
}
interface IDrillDownDimensionItem { interface IDrillDownDimensionItem {
dimensionId: number; dimensionId: number;
necessary?: boolean; necessary?: boolean;
@@ -201,7 +235,10 @@ export declare namespace ISemantic {
hasAdminRes: boolean; hasAdminRes: boolean;
type: string; type: string;
tags: string[]; tags: string[];
typeParams: ITypeParams; // typeParams: IMeasureTypeParams;
metricDefineByMeasureParams: IMeasureTypeParams;
metricDefineByFieldParams: IFieldTypeParams;
metricDefineByMetricParams: IMetricTypeParams;
fullPath: string; fullPath: string;
dataFormatType: string; dataFormatType: string;
dataFormat: string; dataFormat: string;

View File

@@ -125,14 +125,14 @@ export function queryMetric(data: any): Promise<any> {
return request.post(`${process.env.API_BASE_URL}metric/queryMetric`, queryParams); return request.post(`${process.env.API_BASE_URL}metric/queryMetric`, queryParams);
} }
export function creatExprMetric(data: any): Promise<any> { export function createMetric(data: any): Promise<any> {
return request.post(`${process.env.API_BASE_URL}metric/creatExprMetric`, { return request.post(`${process.env.API_BASE_URL}metric/createMetric`, {
data, data,
}); });
} }
export function updateExprMetric(data: any): Promise<any> { export function updateMetric(data: any): Promise<any> {
return request.post(`${process.env.API_BASE_URL}metric/updateExprMetric`, { return request.post(`${process.env.API_BASE_URL}metric/updateMetric`, {
data, data,
}); });
} }
@@ -430,6 +430,12 @@ export function getModelDetail(data: any): Promise<any> {
return request.get(`${process.env.API_BASE_URL}model/getModel/${data.modelId}`); return request.get(`${process.env.API_BASE_URL}model/getModel/${data.modelId}`);
} }
export function getMetricsToCreateNewMetric(data: any): Promise<any> {
return request.get(
`${process.env.API_BASE_URL}metric/getMetricsToCreateNewMetric/${data.modelId}`,
);
}
export function createDictTask(data: any): Promise<any> { export function createDictTask(data: any): Promise<any> {
return request(`${process.env.CHAT_API_BASE_URL}dict/task`, { return request(`${process.env.CHAT_API_BASE_URL}dict/task`, {
method: 'POST', method: 'POST',
@@ -546,3 +552,11 @@ export function metricStarState(data: { id: number; state: boolean }): Promise<a
}); });
} }
} }
export function getDatabaseParameters(): Promise<any> {
return request.get(`${process.env.API_BASE_URL}database/getDatabaseParameters`);
}
export function getDatabaseDetail(id: number): Promise<any> {
return request.get(`${process.env.API_BASE_URL}database/${id}`);
}

View File

@@ -1,6 +1,11 @@
import type { API } from '@/services/API'; import type { API } from '@/services/API';
import { ISemantic } from './data'; import { ISemantic } from './data';
import type { DataNode } from 'antd/lib/tree'; import type { DataNode } from 'antd/lib/tree';
import { Form, Input, InputNumber, Switch, Select } from 'antd';
import FormItemTitle from '@/components/FormHelper/FormItemTitle';
import { ConfigParametersItem } from '../System/types';
const FormItem = Form.Item;
const { TextArea } = Input;
export const changeTreeData = (treeData: API.DomainList, auth?: boolean): DataNode[] => { export const changeTreeData = (treeData: API.DomainList, auth?: boolean): DataNode[] => {
return treeData.map((item: any) => { return treeData.map((item: any) => {
@@ -125,3 +130,68 @@ export const findLeafNodesFromDomainList = (
return leafNodes; return leafNodes;
}; };
export const genneratorFormItemList = (itemList: ConfigParametersItem[]) => {
return itemList.map((item) => {
const { dataType, name, comment, placeholder, description, require } = item;
let defaultItem = <Input />;
switch (dataType) {
case 'string':
if (name === 'password') {
defaultItem = <Input.Password placeholder={placeholder} />;
} else {
defaultItem = <Input placeholder={placeholder} />;
}
break;
case 'longText':
defaultItem = <TextArea placeholder={placeholder} style={{ height: 100 }} />;
break;
case 'number':
defaultItem = <InputNumber placeholder={placeholder} style={{ width: '100%' }} />;
break;
case 'bool':
return (
<FormItem
name={name}
label={comment}
key={name}
valuePropName="checked"
getValueFromEvent={(value) => {
return value === true ? 'true' : 'false';
}}
getValueProps={(value) => {
return {
checked: value === 'true',
};
}}
>
<Switch />
</FormItem>
);
case 'list': {
const { candidateValues = [] } = item;
const options = candidateValues.map((value) => {
return { label: value, value };
});
defaultItem = (
<Select style={{ width: '100%' }} options={options} placeholder={placeholder} />
);
break;
}
default:
defaultItem = <Input placeholder={placeholder} />;
break;
}
return (
<FormItem
name={name}
key={name}
rules={[{ required: !!require, message: `请输入${comment}` }]}
label={<FormItemTitle title={comment} subTitle={description} />}
>
{defaultItem}
</FormItem>
);
});
};

View File

@@ -17,21 +17,19 @@ import React, { useState, useEffect } from 'react';
import { getSystemConfig, saveSystemConfig } from '@/services/user'; import { getSystemConfig, saveSystemConfig } from '@/services/user';
import ProCard from '@ant-design/pro-card'; import ProCard from '@ant-design/pro-card';
import SelectTMEPerson from '@/components/SelectTMEPerson'; import SelectTMEPerson from '@/components/SelectTMEPerson';
import { SystemConfigParametersItem, SystemConfig } from './types'; import { ConfigParametersItem, SystemConfig } from './types';
import FormItemTitle from '@/components/FormHelper/FormItemTitle'; import FormItemTitle from '@/components/FormHelper/FormItemTitle';
import { groupBy } from 'lodash'; import { groupBy } from 'lodash';
import { genneratorFormItemList } from '../SemanticModel/utils';
const FormItem = Form.Item; const FormItem = Form.Item;
const { TextArea } = Input; const { TextArea } = Input;
const System: React.FC = () => { const System: React.FC = () => {
const [systemConfig, setSystemConfig] = useState<Record<string, SystemConfigParametersItem[]>>( const [systemConfig, setSystemConfig] = useState<Record<string, ConfigParametersItem[]>>({});
{},
);
const [anchorItems, setAnchorItems] = useState<{ key: string; href: string; title: string }[]>( const [anchorItems, setAnchorItems] = useState<{ key: string; href: string; title: string }[]>(
[], [],
); );
const [configSource, setConfigSource] = useState<SystemConfig>(); const [configSource, setConfigSource] = useState<SystemConfig>();
const [paramDescMap, setParamDescMap] = useState<Record<string, string>>({});
useEffect(() => { useEffect(() => {
querySystemConfig(); querySystemConfig();
@@ -52,25 +50,13 @@ const System: React.FC = () => {
setAnchorItems(anchor); setAnchorItems(anchor);
setSystemConfig(groupByConfig); setSystemConfig(groupByConfig);
setInitData(admins, parameters); setInitData(admins, parameters);
initDescMap(parameters);
setConfigSource(data); setConfigSource(data);
} else { } else {
message.error(msg); message.error(msg);
} }
}; };
const initDescMap = (systemConfigParameters: SystemConfigParametersItem[]) => { const setInitData = (admins: string[], systemConfigParameters: ConfigParametersItem[]) => {
const descData = systemConfigParameters.reduce(
(descMap: Record<string, string>, item: SystemConfigParametersItem) => {
descMap[item.name] = item.description;
return descMap;
},
{},
);
setParamDescMap(descData);
};
const setInitData = (admins: string[], systemConfigParameters: SystemConfigParametersItem[]) => {
const fieldsValue = systemConfigParameters.reduce( const fieldsValue = systemConfigParameters.reduce(
(fields, item) => { (fields, item) => {
const { name, value } = item; const { name, value } = item;
@@ -95,7 +81,6 @@ const System: React.FC = () => {
return { return {
...item, ...item,
value: submitData[name], value: submitData[name],
description: paramDescMap[name],
}; };
} }
return item; return item;
@@ -143,69 +128,7 @@ const System: React.FC = () => {
bordered bordered
id={key} id={key}
> >
{itemList.map((item) => { {genneratorFormItemList(itemList)}
const { dataType, name, comment } = item;
let defaultItem = <Input />;
switch (dataType) {
case 'string':
defaultItem = <TextArea placeholder="" style={{ height: 100 }} />;
break;
case 'number':
defaultItem = <InputNumber style={{ width: '100%' }} />;
break;
case 'bool':
return (
<FormItem
name={name}
label={comment}
key={name}
valuePropName="checked"
getValueFromEvent={(value) => {
return value === true ? 'true' : 'false';
}}
getValueProps={(value) => {
return {
checked: value === 'true',
};
}}
>
<Switch />
</FormItem>
);
case 'list': {
const { candidateValues = [] } = item;
const options = candidateValues.map((value) => {
return { label: value, value };
});
defaultItem = <Select style={{ width: '100%' }} options={options} />;
break;
}
default:
defaultItem = <Input />;
break;
}
return (
<FormItem
name={name}
key={name}
label={
<FormItemTitle
title={comment}
subTitle={paramDescMap[name]}
// subTitleEditable={true}
onSubTitleChange={(title) => {
setParamDescMap({
...paramDescMap,
[name]: title,
});
}}
/>
}
>
{defaultItem}
</FormItem>
);
})}
</ProCard> </ProCard>
); );
})} })}

View File

@@ -1,15 +1,17 @@
export type SystemConfigParametersItem = { export type ConfigParametersItem = {
dataType: string; dataType: string;
name: string; name: string;
comment: string; comment: string;
value: string; value: string;
candidateValues: string[]; candidateValues: string[];
description: string; description: string;
require?: boolean;
placeholder?: string;
}; };
export type SystemConfig = { export type SystemConfig = {
id: number; id: number;
admin: string; admin: string;
admins: string[]; admins: string[];
parameters: SystemConfigParametersItem[]; parameters: ConfigParametersItem[];
}; };