diff --git a/webapp/packages/supersonic-fe/config/routes.ts b/webapp/packages/supersonic-fe/config/routes.ts index e7c62b84e..efeb253da 100644 --- a/webapp/packages/supersonic-fe/config/routes.ts +++ b/webapp/packages/supersonic-fe/config/routes.ts @@ -40,13 +40,7 @@ const ROUTES = [ name: 'semanticModel', envEnableList: [ENV_KEY.SEMANTIC], }, - { - path: '/database', - name: 'database', - hideInMenu: true, - component: './SemanticModel/components/Database/DatabaseTable', - envEnableList: [ENV_KEY.SEMANTIC], - }, + { path: '/metric', name: 'metric', @@ -85,6 +79,13 @@ const ROUTES = [ hideInMenu: true, component: './Login', }, + { + path: '/database', + name: 'database', + // hideInMenu: true, + component: './SemanticModel/components/Database/DatabaseTable', + envEnableList: [ENV_KEY.SEMANTIC], + }, { path: '/system', name: 'system', diff --git a/webapp/packages/supersonic-fe/src/components/DisabledWheelNumberInput/index.tsx b/webapp/packages/supersonic-fe/src/components/DisabledWheelNumberInput/index.tsx new file mode 100644 index 000000000..cb7192d84 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/components/DisabledWheelNumberInput/index.tsx @@ -0,0 +1,21 @@ +import React, { useEffect, useRef } from 'react'; +import { InputNumber } from 'antd'; + +const DisabledWheelNumberInput: React.FC = ({ ...rest }) => { + const ref = useRef(null); + + useEffect(() => { + if (ref.current) { + ref.current.addEventListener('wheel', handleWheel); + } + }, []); + + const handleWheel = (event) => { + event.stopPropagation(); + event.preventDefault(); + }; + + return ; +}; + +export default DisabledWheelNumberInput; diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/components/DataSourceFieldForm.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/components/DataSourceFieldForm.tsx index cdcfaf9a7..f86970669 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/components/DataSourceFieldForm.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/components/DataSourceFieldForm.tsx @@ -298,7 +298,7 @@ const DataSourceFieldForm: React.FC = ({ fields, sql, onFieldChange, onSq return ( <> - = ({ fields, sql, onFieldChange, onSq 为了保障同一个模型下维度/指标列表唯一,消除歧义,若本模型下的多个数据源存在相同的字段名并且都勾选了快速创建,系统默认这些相同字段的指标维度是同一个,同时列表中将只显示第一次创建的指标/维度。 } - /> + /> */} dataSource={fields} columns={columns} diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/Detail.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/Detail.tsx index f436e1e13..815bb2b5d 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/Detail.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/Detail.tsx @@ -1,13 +1,15 @@ -import { message } from 'antd'; +import { message, Tabs, Button, Space } from 'antd'; import React, { useState, useEffect } from 'react'; import { getMetricData, getDimensionList, getDrillDownDimension } from '../service'; -import { connect, useParams } from 'umi'; +import { connect, useParams, history } from 'umi'; import type { StateType } from '../model'; import styles from './style.less'; +import { ArrowLeftOutlined } from '@ant-design/icons'; import MetricTrendSection from '@/pages/SemanticModel/Metric/components/MetricTrendSection'; import { ISemantic } from '../data'; import DimensionAndMetricRelationModal from '../components/DimensionAndMetricRelationModal'; import MetricInfoSider from './MetricInfoSider'; +import type { TabsProps } from 'antd'; type Props = Record; @@ -17,6 +19,9 @@ const MetricDetail: React.FC = () => { const [metricRelationModalOpenState, setMetricRelationModalOpenState] = useState(false); const [metircData, setMetircData] = useState(); const [dimensionList, setDimensionList] = useState([]); + const [drillDownDimension, setDrillDownDimension] = useState( + [], + ); const [relationDimensionOptions, setRelationDimensionOptions] = useState< { value: string; label: string; modelId: number }[] >([]); @@ -38,6 +43,7 @@ const MetricDetail: React.FC = () => { const queryDrillDownDimension = async (metricId: number) => { const { code, data, msg } = await getDrillDownDimension(metricId); if (code === 200 && Array.isArray(data)) { + setDrillDownDimension(data); const ids = data.map((item) => item.dimensionId); queryDimensionList(ids); return data; @@ -70,15 +76,57 @@ const MetricDetail: React.FC = () => { return []; }; + const tabItems: TabsProps['items'] = [ + { + key: 'metricTrend', + label: '图表', + children: ( + + ), + }, + // { + // key: 'metricCaliberInput', + // label: '基础信息', + // children: <>, + // }, + // { + // key: 'metricDataRemark', + // label: '备注', + // children: <>, + // }, + ]; + return ( <>
- { + history.push('/metric/market'); + }} + > + + + 返回列表页 + + + ), + }} + size="large" + className={styles.metricDetailTab} />
@@ -91,20 +139,20 @@ const MetricDetail: React.FC = () => { />
+ { + setMetricRelationModalOpenState(false); + }} + onSubmit={(relations) => { + queryMetricData(metricId); + queryDrillDownDimension(metricId); + setMetricRelationModalOpenState(false); + }} + />
- { - setMetricRelationModalOpenState(false); - }} - onSubmit={(relations) => { - queryMetricData(metricId); - queryDrillDownDimension(metricId); - setMetricRelationModalOpenState(false); - }} - /> ); }; diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/Market.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/Market.tsx index 260c2db0c..1b17ef965 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/Market.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/Market.tsx @@ -21,7 +21,6 @@ import moment from 'moment'; import styles from './style.less'; import { ISemantic } from '../data'; import BatchCtrlDropDownButton from '@/components/BatchCtrlDropDownButton'; -import MetricStar from './components/MetricStar'; import { ColumnsConfig } from '../components/MetricTableColumnRender'; type Props = { @@ -161,13 +160,19 @@ const ClassMetricTable: React.FC = ({ domainManger, dispatch }) => { }; const columns: ProColumns[] = [ - // { - // dataIndex: 'id', - // title: 'ID', - // }, + { + dataIndex: 'id', + title: 'ID', + width: 80, + fixed: 'left', + search: false, + }, { dataIndex: 'name', title: '指标', + // width: '20%', + width: 280, + fixed: 'left', render: ColumnsConfig.metricInfo.render, }, { @@ -190,16 +195,32 @@ const ClassMetricTable: React.FC = ({ domainManger, dispatch }) => { return <> {record.modelName}; }, }, + { + dataIndex: 'sensitiveLevel', + title: '敏感度', + width: 150, + valueEnum: SENSITIVE_LEVEL_ENUM, + render: ColumnsConfig.sensitiveLevel.render, + }, + + { + dataIndex: 'description', + title: '描述', + search: false, + width: 300, + render: ColumnsConfig.description.render, + }, { dataIndex: 'status', title: '状态', - width: 120, + width: 180, search: false, render: ColumnsConfig.state.render, }, { - dataIndex: 'description', - title: '描述', + dataIndex: 'createdBy', + title: '创建人', + // width: 150, search: false, }, { @@ -214,6 +235,7 @@ const ClassMetricTable: React.FC = ({ domainManger, dispatch }) => { title: '操作', dataIndex: 'x', valueType: 'option', + width: 180, render: (_, record) => { if (record.hasAdminRes) { return ( @@ -334,7 +356,7 @@ const ClassMetricTable: React.FC = ({ domainManger, dispatch }) => { metricList={dataSource} disabledEdit={true} onMetricChange={(metricItem: ISemantic.IMetricItem) => { - history.push(`/metric/detail/${metricItem.modelId}/${metricItem.bizName}`); + history.push(`/metric/detail/${metricItem.id}`); }} onDeleteBtnClick={(metricItem: ISemantic.IMetricItem) => { deleteMetricQuery(metricItem.id); @@ -355,6 +377,7 @@ const ClassMetricTable: React.FC = ({ domainManger, dispatch }) => { columns={columns} pagination={pagination} size="large" + scroll={{ x: 1500 }} tableAlertRender={() => { return false; }} diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/MetricInfoSider.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/MetricInfoSider.tsx index 81d71110a..2fa8e9ede 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/MetricInfoSider.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/MetricInfoSider.tsx @@ -1,4 +1,4 @@ -import { Tag, Space, Tooltip } from 'antd'; +import { Tag, Space, Tooltip, Typography } from 'antd'; import React from 'react'; import { connect } from 'umi'; import type { StateType } from '../model'; @@ -11,13 +11,15 @@ import { PartitionOutlined, PlusOutlined, AreaChartOutlined, - DeleteOutlined, } from '@ant-design/icons'; import styles from './style.less'; +import { isString } from 'lodash'; import { SENSITIVE_LEVEL_ENUM, SENSITIVE_LEVEL_COLOR } from '../constant'; import { ISemantic } from '../data'; import MetricStar from './components/MetricStar'; +const { Text } = Typography; + type Props = { metircData: ISemantic.IMetricItem; domainManger: StateType; @@ -40,7 +42,6 @@ const MetricInfoSider: React.FC = ({ {metircData?.name} - {metircData?.alias && `[${metircData.alias}]`} {metircData?.hasAdminRes && ( = ({ + + {isArrayOfValues(metircData?.tags) && ( +
+ 别名: + + + {isString(metircData?.alias) && + metircData?.alias.split(',').map((aliasName: string) => { + return ( + + + {aliasName} + + + ); + })} + + +
+ )} +
描述: {metircData?.description} diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/components/MetricFilter.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/components/MetricFilter.tsx index 4b29065e8..8990cd7e9 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/components/MetricFilter.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/components/MetricFilter.tsx @@ -98,11 +98,11 @@ const MetricFilter: React.FC = ({ initFilterValues = {}, onFiltersChange
- + {/* - + */} {/* diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/components/MetricTrendDimensionFilter.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/components/MetricTrendDimensionFilter.tsx index f3a9d5f0a..ca1ca98dd 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/components/MetricTrendDimensionFilter.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/components/MetricTrendDimensionFilter.tsx @@ -5,15 +5,17 @@ import RemoteSelect, { RemoteSelectImperativeHandle } from '@/components/RemoteS import { queryDimValue } from '@/pages/SemanticModel/service'; import { OperatorEnum } from '@/pages/SemanticModel/enum'; import { isString } from 'lodash'; +import { isArrayOfValues } from '@/utils/utils'; const FormItem = Form.Item; type Props = { - dimensionOptions: OptionsItem[]; + dimensionOptions: (OptionsItem & { modelId: number })[]; modelId: number; periodDate?: { startDate: string; endDate: string; dateField: string }; value?: FormData; onChange?: (value: FormData) => void; + afterSolt?: React.ReactNode; }; export type FormData = { @@ -24,16 +26,15 @@ export type FormData = { const MetricTrendDimensionFilter: React.FC = ({ dimensionOptions, - modelId, value, periodDate, + afterSolt, onChange, }) => { const [form] = Form.useForm(); const dimensionValueSearchRef = useRef(); const queryParams = useRef<{ dimensionBizName?: string }>({}); const [formData, setFormData] = useState({ operator: OperatorEnum.IN } as FormData); - useEffect(() => { if (!value) { return; @@ -59,7 +60,10 @@ const MetricTrendDimensionFilter: React.FC = ({ return; } const { dimensionBizName } = queryParams.current; - const targetOptions = dimensionOptions.find((item) => item.value === dimensionBizName) || {}; + const targetOptions = dimensionOptions.find((item) => item.value === dimensionBizName); + if (!targetOptions) { + return; + } const { code, data } = await queryDimValue({ ...queryParams.current, value: searchValue, @@ -95,7 +99,6 @@ const MetricTrendDimensionFilter: React.FC = ({ }} onValuesChange={(value, values) => { const { operator, dimensionValue } = values; - if (multipleValueOperator.includes(operator) && isString(dimensionValue)) { const tempDimensionValue = [dimensionValue]; setFormData({ ...values, dimensionValue: tempDimensionValue }); @@ -152,12 +155,14 @@ const MetricTrendDimensionFilter: React.FC = ({ + } /> @@ -221,31 +233,21 @@ const MetricTrendSection: React.FC = ({ - - {/*
+ {/* -
*/} + */} - {authMessage &&
{authMessage}
} +
- + + 数据趋势 + {authMessage &&
{authMessage}
} + + } + > = ({
-
+
= ({ } > -
+
= ({ return (
- {Array.isArray(columns) && columns.length > 0 && ( - {}} - /> - )} + {/* {Array.isArray(columns) && columns.length > 0 && ( */} +
{}} + /> + {/* )} */} ); }; diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/style.less b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/style.less index 5afd18ee9..a1cd7c4e4 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/style.less +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Metric/style.less @@ -121,6 +121,23 @@ .metricDetailWrapper { height: calc(100vh - 56px); overflow: scroll; + .metricDetailTab { + :global { + .ant-tabs-nav { + background-color: rgb(255, 255, 255); + transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + margin: 10px 20px 0 20px; + padding: 0 20px; + border-radius: 8px; + } + .ant-tabs-tab { + padding: 12px 0; + color: #344767; + font-weight: 500; + } + } + + } .metricDetail { display: flex; padding: 0px; @@ -158,27 +175,27 @@ .siderContainer { background-color: rgb(255, 255, 255); width: 450px; - min-height: 100vh; - margin: 20px 20px 20px 0; + min-height: calc(100vh - 78px); + margin: 10px 20px 20px 0; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.08) 6px 0px 16px 0px, rgba(0, 0, 0, 0.12) 3px 0px 6px -4px, rgba(0, 0, 0, 0.05) 9px 0px 28px 8px; } } } -.metricTrendSection { - .sectionBox { - margin: 20px; - padding: 10px; - box-shadow: #888888 0px 0px 1px, rgba(29, 41, 57, 0.08) 0px 1px 3px; - background-color: rgb(255, 255, 255); - transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; - border-radius: 6px; - background-image: none; - overflow: hidden; - position: relative; - } + +.sectionBox { + margin: 20px; + padding: 10px; + box-shadow: #888888 0px 0px 1px, rgba(29, 41, 57, 0.08) 0px 1px 3px; + background-color: rgb(255, 255, 255); + transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + border-radius: 6px; + background-image: none; + overflow: hidden; + position: relative; } + .metricInfoSider{ padding: 24px; color: #344767; diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/index.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/index.tsx index 0eff26eec..0eebba6e5 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/index.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/SemanticGraph/index.tsx @@ -103,12 +103,14 @@ const DomainManger: React.FC = ({ domainManger, dispatch }) => { const filterData = dataSourceRef.current.reduce( (data: ISemantic.IDomainSchemaRelaList, item: ISemantic.IDomainSchemaRelaItem) => { const { dimensions, metrics } = item; - const dimensionsList = dimensions.filter((dimension) => { - return dimension.name.includes(text); - }); - const metricsList = metrics.filter((metric) => { - return metric.name.includes(text); - }); + const dimensionsList = + dimensions?.filter((dimension) => { + return dimension.name.includes(text); + }) || []; + const metricsList = + metrics?.filter((metric) => { + return metric.name.includes(text); + }) || []; data.push({ ...item, dimensions: dimensionsList, diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/View/components/DimensionMetricTransferModal.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/View/components/DimensionMetricTransferModal.tsx new file mode 100644 index 000000000..ac60fc74e --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/View/components/DimensionMetricTransferModal.tsx @@ -0,0 +1,165 @@ +import React, { useState, useEffect } from 'react'; + +import { ISemantic } from '../../data'; + +import { TransType } from '../../enum'; +import DimensionMetricVisibleTransfer from '../../components/Entity/DimensionMetricVisibleTransfer'; +import { wrapperTransTypeAndId } from '../../components/Entity/utils'; + +export type ModelCreateFormModalProps = { + dimensionList: ISemantic.IDimensionItem[]; + metricList: ISemantic.IMetricItem[]; + modelId?: number; + selectedTransferKeys: React.Key[]; + onCancel: () => void; + onSubmit: (values: any, selectedKeys: React.Key[]) => void; +}; + +const DimensionMetricTransferModal: React.FC = ({ + modelId, + selectedTransferKeys, + metricList, + dimensionList, + onSubmit, +}) => { + const [sourceList, setSourceList] = useState([]); + const [selectedItemList, setSelectedItemList] = useState([]); + + const addItemKey = (item: any, transType: TransType) => { + const { id } = item; + const key = wrapperTransTypeAndId(transType, id); + return { + ...item, + transType, + key, + }; + }; + + useEffect(() => { + const sourceDimensionList = dimensionList.reduce((mergeList: any[], item) => { + mergeList.push(addItemKey(item, TransType.DIMENSION)); + return mergeList; + }, []); + + const hasDimensionList = selectedItemList + .filter((item) => { + return item.typeEnum === TransType.DIMENSION; + }) + .reduce((modelDimensionList: ISemantic.IDimensionItem[], item) => { + const hasItem = sourceDimensionList.find((dataListItem: ISemantic.IDimensionItem) => { + return dataListItem.id === item.id; + }); + if (!hasItem) { + modelDimensionList.push(addItemKey(item, TransType.DIMENSION)); + } + return modelDimensionList; + }, []); + + const sourceMetricList = metricList.reduce((mergeList: any[], item) => { + mergeList.push(addItemKey(item, TransType.METRIC)); + return mergeList; + }, []); + + const hasMetricList = selectedItemList + .filter((item) => { + return item.typeEnum === TransType.METRIC; + }) + .reduce((modelMetricList: ISemantic.IMetricItem[], item) => { + const hasItem = sourceMetricList.find((dataListItem: ISemantic.IMetricItem) => { + return dataListItem.id === item.id; + }); + if (!hasItem) { + modelMetricList.push(addItemKey(item, TransType.METRIC)); + } + return modelMetricList; + }, []); + + setSourceList([ + ...sourceDimensionList, + ...sourceMetricList, + ...hasDimensionList, + ...hasMetricList, + ]); + }, [dimensionList, metricList]); + + return ( + { + const removeDimensionList: ISemantic.IDimensionItem[] = []; + const removeMetricList: ISemantic.IMetricItem[] = []; + const dimensionItemChangeList = dimensionList.reduce( + (dimensionChangeList: any[], item: any) => { + if (newTargetKeys.includes(wrapperTransTypeAndId(TransType.DIMENSION, item.id))) { + dimensionChangeList.push(item); + } else { + removeDimensionList.push(item.id); + } + return dimensionChangeList; + }, + [], + ); + + const metricItemChangeList = metricList.reduce((metricChangeList: any[], item: any) => { + if (newTargetKeys.includes(wrapperTransTypeAndId(TransType.METRIC, item.id))) { + metricChangeList.push(item); + } else { + removeMetricList.push(item.id); + } + return metricChangeList; + }, []); + + setSelectedItemList([...dimensionItemChangeList, ...metricItemChangeList]); + + // 如果不是当前选中model中的指标或者维度,则先从本地数据中删除,避免后续请求数据更新时产生视觉上的界面闪烁 + const preUpdateSourceData = sourceList.filter((item) => { + const { typeEnum, id } = item; + if (typeEnum === TransType.DIMENSION) { + if (modelId !== item.modelId && removeDimensionList.includes(id)) { + return false; + } + } + if (typeEnum === TransType.METRIC) { + if (modelId !== item.modelId && removeMetricList.includes(id)) { + return false; + } + } + return true; + }); + setSourceList([...preUpdateSourceData]); + + const viewModelConfigs = [...dimensionItemChangeList, ...metricItemChangeList].reduce( + (config, item) => { + const { modelId, id, typeEnum } = item; + if (config[modelId]) { + if (typeEnum === TransType.DIMENSION) { + config[modelId].dimensions.push(id); + } + if (typeEnum === TransType.METRIC) { + config[modelId].metrics.push(id); + } + } else { + config[modelId] = { + id: modelId, + metrics: typeEnum === TransType.METRIC ? [id] : [], + dimensions: typeEnum === TransType.DIMENSION ? [id] : [], + }; + } + return config; + }, + {}, + ); + + onSubmit?.(viewModelConfigs, newTargetKeys); + }} + /> + ); +}; + +export default DimensionMetricTransferModal; diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/View/components/MetricInfoCreateForm.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/View/components/MetricInfoCreateForm.tsx new file mode 100644 index 000000000..f877242ee --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/View/components/MetricInfoCreateForm.tsx @@ -0,0 +1,773 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { + Form, + Button, + Modal, + Steps, + Input, + Select, + Radio, + Switch, + InputNumber, + message, + Result, + Row, + Col, + Space, + Tooltip, + Tag, +} from 'antd'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import MetricMeasuresFormTable from './MetricMeasuresFormTable'; +import { SENSITIVE_LEVEL_OPTIONS, METRIC_DEFINE_TYPE } from '../constant'; +import { formLayout } from '@/components/FormHelper/utils'; +import FormItemTitle from '@/components/FormHelper/FormItemTitle'; +import styles from './style.less'; +import { getMetricsToCreateNewMetric, getModelDetail, getDrillDownDimension } from '../service'; +import MetricMetricFormTable from './MetricMetricFormTable'; +import MetricFieldFormTable from './MetricFieldFormTable'; +import DimensionAndMetricRelationModal from './DimensionAndMetricRelationModal'; +import TableTitleTooltips from '../components/TableTitleTooltips'; +import { createMetric, updateMetric, mockMetricAlias, getMetricTags } from '../service'; +import { ISemantic } from '../data'; +import { history } from 'umi'; + +export type CreateFormProps = { + datasourceId?: number; + domainId: number; + modelId: number; + createModalVisible: boolean; + metricItem: any; + onCancel?: () => void; + onSubmit?: (values: any) => void; +}; + +const { Step } = Steps; +const FormItem = Form.Item; +const { TextArea } = Input; +const { Option } = Select; + +const queryParamsTypeParamsKey = { + [METRIC_DEFINE_TYPE.MEASURE]: 'metricDefineByMeasureParams', + [METRIC_DEFINE_TYPE.METRIC]: 'metricDefineByMetricParams', + [METRIC_DEFINE_TYPE.FIELD]: 'metricDefineByFieldParams', +}; + +const MetricInfoCreateForm: React.FC = ({ + datasourceId, + domainId, + modelId, + onCancel, + createModalVisible, + metricItem, + onSubmit, +}) => { + const isEdit = !!metricItem?.id; + const [currentStep, setCurrentStep] = useState(0); + const formValRef = useRef({} as any); + const [form] = Form.useForm(); + const updateFormVal = (val: any) => { + const formVal = { + ...formValRef.current, + ...val, + }; + formValRef.current = formVal; + }; + + const [classMeasureList, setClassMeasureList] = useState([]); + + 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 [exprTypeParamsState, setExprTypeParamsState] = useState([]); + + const [defineType, setDefineType] = useState(METRIC_DEFINE_TYPE.MEASURE); + + const [createNewMetricList, setCreateNewMetricList] = useState([]); + const [fieldList, setFieldList] = useState([]); + const [isPercentState, setIsPercentState] = useState(false); + const [isDecimalState, setIsDecimalState] = useState(false); + const [hasMeasuresState, setHasMeasuresState] = useState(true); + const [llmLoading, setLlmLoading] = useState(false); + + const [tagOptions, setTagOptions] = useState<{ label: string; value: string }[]>([]); + + const [metricRelationModalOpenState, setMetricRelationModalOpenState] = useState(false); + + const [drillDownDimensions, setDrillDownDimensions] = useState< + ISemantic.IDrillDownDimensionItem[] + >([]); + + const [drillDownDimensionsConfig, setDrillDownDimensionsConfig] = useState< + ISemantic.IDrillDownDimensionItem[] + >([]); + + const forward = () => setCurrentStep(currentStep + 1); + const backward = () => setCurrentStep(currentStep - 1); + + const queryModelDetail = async () => { + // const { code, data } = await getMeasureListByModelId(modelId); + const { code, data } = await getModelDetail({ modelId: modelId || metricItem?.modelId }); + if (code === 200) { + if (Array.isArray(data?.modelDetail?.fields)) { + setFieldList(data.modelDetail.fields); + } + if (Array.isArray(data?.modelDetail?.measures)) { + setClassMeasureList(data.modelDetail.measures); + if (datasourceId) { + const hasMeasures = data.some( + (item: ISemantic.IMeasure) => item.datasourceId === datasourceId, + ); + setHasMeasuresState(hasMeasures); + } + return; + } + } + setClassMeasureList([]); + }; + + const queryDrillDownDimension = async (metricId: number) => { + const { code, data, msg } = await getDrillDownDimension(metricId); + if (code === 200 && Array.isArray(data)) { + setDrillDownDimensionsConfig(data); + } + if (code !== 200) { + message.error(msg); + } + return []; + }; + + useEffect(() => { + queryModelDetail(); + queryMetricsToCreateNewMetric(); + queryMetricTags(); + }, []); + + const handleNext = async () => { + const fieldsValue = await form.validateFields(); + const submitForm = { + ...formValRef.current, + ...fieldsValue, + metricDefineType: defineType, + [queryParamsTypeParamsKey[defineType]]: exprTypeParamsState[defineType], + }; + updateFormVal(submitForm); + if (currentStep < 1) { + forward(); + } else { + await saveMetric(submitForm); + } + }; + + const initData = () => { + const { + id, + name, + bizName, + description, + sensitiveLevel, + typeParams, + dataFormat, + dataFormatType, + alias, + tags, + metricDefineType, + metricDefineByMeasureParams, + metricDefineByMetricParams, + metricDefineByFieldParams, + } = metricItem; + const isPercent = dataFormatType === 'percent'; + const isDecimal = dataFormatType === 'decimal'; + const initValue = { + id, + name, + bizName, + sensitiveLevel, + description, + tags, + // isPercent, + dataFormatType: dataFormatType || '', + alias: alias && alias.trim() ? alias.split(',') : [], + dataFormat: dataFormat || { + decimalPlaces: 2, + needMultiply100: false, + }, + }; + const editInitFormVal = { + ...formValRef.current, + ...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); + form.setFieldsValue(initValue); + setDefineType(metricDefineType); + setIsPercentState(isPercent); + setIsDecimalState(isDecimal); + queryDrillDownDimension(metricItem?.id); + }; + + useEffect(() => { + if (isEdit) { + initData(); + } + }, [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 queryParams = { + modelId: isEdit ? metricItem.modelId : modelId, + relateDimension: { + ...(metricItem?.relateDimension || {}), + drillDownDimensions, + }, + ...fieldsValue, + }; + const { alias, dataFormatType } = queryParams; + queryParams.alias = Array.isArray(alias) ? alias.join(',') : ''; + if (!queryParams[queryParamsTypeParamsKey[defineType]]?.expr) { + message.error('请输入度量表达式'); + return; + } + if (!dataFormatType) { + delete queryParams.dataFormat; + } + if (isEmptyConditions(defineType, queryParams[queryParamsTypeParamsKey[defineType]])) { + return; + } + + let saveMetricQuery = createMetric; + if (queryParams.id) { + saveMetricQuery = updateMetric; + } + const { code, msg } = await saveMetricQuery(queryParams); + if (code === 200) { + message.success('编辑指标成功'); + onSubmit?.(queryParams); + return; + } + message.error(msg); + }; + + const generatorMetricAlias = async () => { + setLlmLoading(true); + const { code, data } = await mockMetricAlias({ ...metricItem }); + const formAlias = form.getFieldValue('alias'); + setLlmLoading(false); + if (code === 200) { + form.setFieldValue('alias', Array.from(new Set([...formAlias, ...data]))); + } else { + message.error('大语言模型解析异常'); + } + }; + + const queryMetricTags = async () => { + const { code, data } = await getMetricTags(); + if (code === 200) { + // form.setFieldValue('alias', Array.from(new Set([...formAlias, ...data]))); + setTagOptions( + Array.isArray(data) + ? data.map((tag: string) => { + return { label: tag, value: tag }; + }) + : [], + ); + } else { + message.error('获取指标标签失败'); + } + }; + const queryMetricsToCreateNewMetric = async () => { + const { code, data } = await getMetricsToCreateNewMetric({ + modelId: modelId || metricItem?.modelId, + }); + if (code === 200) { + setCreateNewMetricList(data); + } else { + message.error('获取指标标签失败'); + } + }; + + const renderContent = () => { + if (currentStep === 1) { + return ( +
+
+ { + setDefineType(e.target.value); + }} + > + 按度量 + 按指标 + 按字段 + +
+ {defineType === METRIC_DEFINE_TYPE.MEASURE && ( + <> + { + setExprTypeParamsState((prevState) => { + return { + ...prevState, + [METRIC_DEFINE_TYPE.MEASURE]: { + ...prevState[METRIC_DEFINE_TYPE.MEASURE], + measures, + }, + }; + }); + }} + onSqlChange={(expr: string) => { + setExprTypeParamsState((prevState) => { + return { + ...prevState, + [METRIC_DEFINE_TYPE.MEASURE]: { + ...prevState[METRIC_DEFINE_TYPE.MEASURE], + expr, + }, + }; + }); + }} + /> + + )} + {defineType === METRIC_DEFINE_TYPE.METRIC && ( + <> +

+ 通过 + + 字段 + + 和 + + 度量 + + 创建的指标可用来创建新的指标 +

+ + { + 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 && ( + <> + { + 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, + }, + }; + }); + }} + /> + + )} +
+ ); + } + + return ( + <> + + + + + + + + + +
+ + + + + + + +

+ 在录入指标时,请务必详细填写指标口径。口径描述对于理解指标的含义、计算方法和使用场景至关重要。一个清晰、准确的口径描述可以帮助其他用户更好地理解和使用该指标,避免因为误解而导致错误的数据分析和决策。在填写口径时,建议包括以下信息: +

+

1. 指标的计算方法:详细说明指标是如何计算的,包括涉及的公式、计算步骤等。

+

2. 数据来源:描述指标所依赖的数据来源,包括数据表、字段等信息。

+

3. 使用场景:说明该指标适用于哪些业务场景,以及如何在这些场景中使用该指标。

+

4. 任何其他相关信息:例如数据更新频率、数据质量要求等。

+

+ 请确保口径描述清晰、简洁且易于理解,以便其他用户能够快速掌握指标的核心要点。 +

+ + } + /> + } + rules={[{ required: true, message: '请输入业务口径' }]} + > +