mirror of
https://github.com/tencentmusic/supersonic.git
synced 2026-04-21 22:34:28 +08:00
[improvement][headless-fe] Revamped the interaction for semantic modeling routing and successfully implemented the switching between dimension and dataset management. (#1934)
Co-authored-by: tristanliu <tristanliu@tencent.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import type { ActionType, ProColumns } from '@ant-design/pro-components';
|
||||
import { ProTable } from '@ant-design/pro-components';
|
||||
import { message, Button, Space, Popconfirm, Input, Tag, Select } from 'antd';
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { useModel } from '@umijs/max';
|
||||
import { useModel, history } from '@umijs/max';
|
||||
import { StatusEnum, SemanticNodeType } from '../enum';
|
||||
import { SENSITIVE_LEVEL_ENUM, SENSITIVE_LEVEL_OPTIONS, TAG_DEFINE_TYPE } from '../constant';
|
||||
import {
|
||||
@@ -19,6 +19,7 @@ import TableHeaderFilter from '@/components/TableHeaderFilter';
|
||||
import BatchCtrlDropDownButton from '@/components/BatchCtrlDropDownButton';
|
||||
import { ColumnsConfig } from './TableColumnRender';
|
||||
import BatchSensitiveLevelModal from '@/components/BatchCtrlDropDownButton/BatchSensitiveLevelModal';
|
||||
import { toDimensionEditPage } from '@/pages/SemanticModel/utils';
|
||||
import styles from './style.less';
|
||||
|
||||
type Props = {};
|
||||
@@ -80,6 +81,9 @@ const ClassDimensionTable: React.FC<Props> = ({}) => {
|
||||
};
|
||||
|
||||
const queryDataSourceList = async () => {
|
||||
if (!domainId) {
|
||||
return;
|
||||
}
|
||||
const { code, data, msg } = await getModelList(domainId);
|
||||
if (code === 200) {
|
||||
setDataSourceList(data);
|
||||
@@ -94,7 +98,7 @@ const ClassDimensionTable: React.FC<Props> = ({}) => {
|
||||
|
||||
useEffect(() => {
|
||||
queryDataSourceList();
|
||||
}, [modelId]);
|
||||
}, [domainId]);
|
||||
|
||||
const queryBatchUpdateStatus = async (ids: React.Key[], status: StatusEnum) => {
|
||||
if (Array.isArray(ids) && ids.length === 0) {
|
||||
@@ -137,7 +141,17 @@ const ClassDimensionTable: React.FC<Props> = ({}) => {
|
||||
message.error(msg);
|
||||
};
|
||||
|
||||
const columnsConfig = ColumnsConfig();
|
||||
// const columnsConfig = ColumnsConfig();
|
||||
const columnsConfig = ColumnsConfig({
|
||||
indicatorInfo: {
|
||||
url: '/model/dimension/:domainId/:modelId/:indicatorId',
|
||||
onNameClick: (record) => {
|
||||
const { id } = record;
|
||||
toDimensionEditPage(domainId, modelId!, id);
|
||||
return false;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const columns: ProColumns[] = [
|
||||
{
|
||||
@@ -216,8 +230,10 @@ const ClassDimensionTable: React.FC<Props> = ({}) => {
|
||||
key="dimensionEditBtn"
|
||||
type="link"
|
||||
onClick={() => {
|
||||
setDimensionItem(record);
|
||||
setCreateModalVisible(true);
|
||||
// setDimensionItem(record);
|
||||
// setCreateModalVisible(true);
|
||||
const { id } = record;
|
||||
toDimensionEditPage(domainId, modelId!, id);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
@@ -423,8 +439,9 @@ const ClassDimensionTable: React.FC<Props> = ({}) => {
|
||||
key="create"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setDimensionItem(undefined);
|
||||
setCreateModalVisible(true);
|
||||
toDimensionEditPage(domainId, modelId!, 0);
|
||||
// setDimensionItem(undefined);
|
||||
// setCreateModalVisible(true);
|
||||
}}
|
||||
>
|
||||
创建维度
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ProTable } from '@ant-design/pro-components';
|
||||
import { message, Button, Space, Popconfirm, Input, Select, Tag } from 'antd';
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { StatusEnum, SemanticNodeType } from '../enum';
|
||||
import { useModel, history } from '@umijs/max';
|
||||
import { useModel } from '@umijs/max';
|
||||
import { SENSITIVE_LEVEL_ENUM, SENSITIVE_LEVEL_OPTIONS, TAG_DEFINE_TYPE } from '../constant';
|
||||
import {
|
||||
queryMetric,
|
||||
@@ -21,6 +21,7 @@ import TableHeaderFilter from '@/components/TableHeaderFilter';
|
||||
import styles from './style.less';
|
||||
import { ISemantic } from '../data';
|
||||
import { ColumnsConfig } from './TableColumnRender';
|
||||
import { toMetricEditPage } from '@/pages/SemanticModel/utils';
|
||||
|
||||
type Props = {
|
||||
onEmptyMetricData?: () => void;
|
||||
@@ -32,7 +33,7 @@ const ClassMetricTable: React.FC<Props> = ({ onEmptyMetricData }) => {
|
||||
const metricModel = useModel('SemanticModel.metricData');
|
||||
const { selectDomainId } = domainModel;
|
||||
const { selectModelId: modelId } = modelModel;
|
||||
const { MrefreshMetricList, selectMetric, setSelectMetric } = metricModel;
|
||||
const { MrefreshMetricList, setSelectMetric } = metricModel;
|
||||
const [batchSensitiveLevelOpenState, setBatchSensitiveLevelOpenState] = useState<boolean>(false);
|
||||
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
|
||||
const [metricItem, setMetricItem] = useState<ISemantic.IMetricItem>();
|
||||
@@ -145,8 +146,8 @@ const ClassMetricTable: React.FC<Props> = ({ onEmptyMetricData }) => {
|
||||
const columnsConfig = ColumnsConfig({
|
||||
indicatorInfo: {
|
||||
url: '/model/metric/:domainId/:modelId/:indicatorId',
|
||||
onNameClick: (record: ISemantic.IMetricItem) => {
|
||||
setSelectMetric(record);
|
||||
onNameClick: (record) => {
|
||||
setSelectMetric(record as ISemantic.IMetricItem);
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -243,9 +244,8 @@ const ClassMetricTable: React.FC<Props> = ({ onEmptyMetricData }) => {
|
||||
type="link"
|
||||
key="metricEditBtn"
|
||||
onClick={() => {
|
||||
history.push(`/model/metric/${record.domainId}/${record.modelId}/${record.id}`);
|
||||
// setMetricItem(record);
|
||||
// setCreateModalVisible(true);
|
||||
const { domainId, modelId, id } = record;
|
||||
toMetricEditPage(domainId, modelId, id);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
@@ -460,8 +460,9 @@ const ClassMetricTable: React.FC<Props> = ({ onEmptyMetricData }) => {
|
||||
key="create"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setMetricItem(undefined);
|
||||
setCreateModalVisible(true);
|
||||
toMetricEditPage(selectDomainId, modelId!, 0);
|
||||
// setMetricItem(undefined);
|
||||
// setCreateModalVisible(true);
|
||||
}}
|
||||
>
|
||||
创建指标
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Button } from 'antd';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { MenuItem } from './type';
|
||||
import styles from './style.less';
|
||||
|
||||
type Props = {
|
||||
detailData?: any;
|
||||
currentMenu: MenuItem;
|
||||
onSave?: (data?: any) => void;
|
||||
} & { children: React.ReactNode };
|
||||
|
||||
const DetailFormWrapper: React.FC<Props> = ({ children, currentMenu, onSave }) => {
|
||||
const [settingKey, setSettingKey] = useState<string>(currentMenu?.key);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentMenu) {
|
||||
setSettingKey(currentMenu.key);
|
||||
}
|
||||
}, [currentMenu]);
|
||||
|
||||
return (
|
||||
<div className={styles.infoCard}>
|
||||
<div className={styles.infoCardTitle}>
|
||||
<span style={{ flex: 'auto' }}>{currentMenu?.text}</span>
|
||||
|
||||
<span style={{ flex: 'none' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
onSave?.();
|
||||
}}
|
||||
>
|
||||
保 存
|
||||
</Button>
|
||||
{/* <Button
|
||||
size="middle"
|
||||
type="link"
|
||||
key="backListBtn"
|
||||
onClick={() => {
|
||||
history.back();
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
<ArrowLeftOutlined />
|
||||
返回列表页
|
||||
</Space>
|
||||
</Button> */}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.infoCardContainer}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailFormWrapper;
|
||||
@@ -0,0 +1,154 @@
|
||||
import { Tag, Space, Tooltip } from 'antd';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
ExportOutlined,
|
||||
SolutionOutlined,
|
||||
PartitionOutlined,
|
||||
SettingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import styles from './style.less';
|
||||
import IndicatorStar from '../IndicatorStar';
|
||||
import { toDomainList, toModelList } from '@/pages/SemanticModel/utils';
|
||||
import { MenuItem } from './type';
|
||||
|
||||
type Props = {
|
||||
detailData: any;
|
||||
menuKey: string;
|
||||
menuList: MenuItem[];
|
||||
onMenuKeyChange?: (key: string, item: MenuItem) => void;
|
||||
};
|
||||
|
||||
const DetailSider: React.FC<Props> = ({ detailData, menuList, menuKey, onMenuKeyChange }) => {
|
||||
const [settingKey, setSettingKey] = useState<string>(menuKey);
|
||||
|
||||
useEffect(() => {
|
||||
if (menuKey) {
|
||||
setSettingKey(menuKey);
|
||||
}
|
||||
}, [menuKey]);
|
||||
|
||||
return (
|
||||
<div className={styles.DetailInfoSider}>
|
||||
<div className={styles.sectionContainer}>
|
||||
{detailData?.id ? (
|
||||
<div className={styles.title}>
|
||||
<div className={styles.name}>
|
||||
<Space>
|
||||
{detailData?.isCollect !== undefined ? (
|
||||
<IndicatorStar indicatorId={detailData?.id} initState={detailData?.isCollect} />
|
||||
) : (
|
||||
<div style={{ width: 15 }}></div>
|
||||
)}
|
||||
|
||||
{detailData?.name}
|
||||
{detailData?.hasAdminRes && (
|
||||
<span
|
||||
className={styles.gotoMetricListIcon}
|
||||
onClick={() => {
|
||||
toModelList(detailData.domainId, detailData.modelId);
|
||||
}}
|
||||
>
|
||||
<Tooltip title="前往所属模型指标列表">
|
||||
<ExportOutlined />
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
{detailData?.bizName && <div className={styles.bizName}>{detailData.bizName}</div>}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.createTitle}>
|
||||
<Space>
|
||||
<SettingOutlined />
|
||||
新建指标
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<hr className={styles.hr} />
|
||||
<div className={styles.section} style={{ padding: '16px 0' }}>
|
||||
<ul className={styles.settingList}>
|
||||
{menuList.map((item) => {
|
||||
return (
|
||||
<li
|
||||
className={item.key === settingKey ? styles.active : ''}
|
||||
key={item.key}
|
||||
onClick={() => {
|
||||
onMenuKeyChange?.(item.key, item);
|
||||
setSettingKey(item.key);
|
||||
}}
|
||||
>
|
||||
<div className={styles.icon}>{item.icon}</div>
|
||||
<div className={styles.content}>
|
||||
<span className={styles.text}> {item.text}</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
{detailData?.id && (
|
||||
<div className={styles.section} style={{ marginTop: 'auto' }}>
|
||||
<div className={styles.sectionTitleBox}>
|
||||
<span className={styles.sectionTitle}>
|
||||
<Space>
|
||||
<SolutionOutlined />
|
||||
创建信息
|
||||
</Space>
|
||||
</span>
|
||||
</div>
|
||||
{detailData?.modelName && (
|
||||
<div className={styles.item}>
|
||||
<span className={styles.itemLable}>所属模型: </span>
|
||||
<span className={styles.itemValue}>
|
||||
<Space>
|
||||
<Tag icon={<PartitionOutlined />} color="#3b5999">
|
||||
{detailData?.modelName || '模型名为空'}
|
||||
</Tag>
|
||||
{detailData?.hasAdminRes && (
|
||||
<span
|
||||
className={styles.gotoMetricListIcon}
|
||||
onClick={() => {
|
||||
toDomainList(detailData.domainId, 'overview');
|
||||
}}
|
||||
>
|
||||
<Tooltip title="前往模型设置页">
|
||||
<ExportOutlined />
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
</Space>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.item}>
|
||||
<span className={styles.itemLable}>创建人: </span>
|
||||
<span className={styles.itemValue}>{detailData?.createdBy}</span>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<span className={styles.itemLable}>创建时间: </span>
|
||||
<span className={styles.itemValue}>
|
||||
{detailData?.createdAt
|
||||
? dayjs(detailData?.createdAt).format('YYYY-MM-DD HH:mm:ss')
|
||||
: ''}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<span className={styles.itemLable}>更新时间: </span>
|
||||
<span className={styles.itemValue}>
|
||||
{detailData?.createdAt
|
||||
? dayjs(detailData?.updatedAt).format('YYYY-MM-DD HH:mm:ss')
|
||||
: ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailSider;
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import styles from './style.less';
|
||||
|
||||
type Props = {
|
||||
siderNode: React.ReactNode;
|
||||
containerNode: React.ReactNode;
|
||||
};
|
||||
|
||||
const DetailContainer: React.FC<Props> = ({ siderNode, containerNode }) => {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.DetailWrapper}>
|
||||
<div className={styles.Detail}>
|
||||
<div className={styles.siderContainer}>{siderNode}</div>
|
||||
<div className={styles.tabContainer}>{containerNode}</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailContainer;
|
||||
@@ -0,0 +1,338 @@
|
||||
.DetailWrapper {
|
||||
.Detail {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
padding: 0px;
|
||||
background-color: transparent;
|
||||
height: 100%;
|
||||
.tabContainer {
|
||||
padding: 12px;
|
||||
min-height: calc(100vh - 105px);
|
||||
width: calc(100vw - 350px);
|
||||
background-color: #fafafb;
|
||||
}
|
||||
.siderContainer {
|
||||
width: 320px;
|
||||
min-height: calc(100vh - 105px);
|
||||
border-radius: 6px;
|
||||
padding: 12px 0 12px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.DetailInfoSider {
|
||||
padding: 10px;
|
||||
color: #344767;
|
||||
background-color: #fff;
|
||||
height: 100%;
|
||||
border: 1px solid #e6ebf1;
|
||||
border-radius: 6px;
|
||||
.createTitle {
|
||||
display: flex;
|
||||
margin-left: 10px;
|
||||
min-height: 47px;
|
||||
margin-bottom: 10px;
|
||||
color:#344767;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
font-family: var(--tencent-font-family);
|
||||
}
|
||||
.gotoMetricListIcon {
|
||||
color: #3182ce;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: #5493ff;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
margin: 10px 0;
|
||||
min-height: 47px;
|
||||
.name {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
}
|
||||
.bizName {
|
||||
margin: 5px 0 0 25px;
|
||||
color: #7b809a;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
.desc {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: #7b809a;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 1.9;
|
||||
}
|
||||
.subTitle {
|
||||
margin: 0px;
|
||||
color: rgb(123, 128, 154);
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
line-height: 1.25;
|
||||
letter-spacing: 0.03333em;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
vertical-align: unset;
|
||||
opacity: 1;
|
||||
}
|
||||
.sectionContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: scroll;
|
||||
|
||||
|
||||
overflow: hidden;
|
||||
background-image: none;
|
||||
border-radius: 6px;
|
||||
.section {
|
||||
padding: 16px;
|
||||
color: rgb(52, 71, 103);
|
||||
line-height: 1.25;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
opacity: 1;
|
||||
.sectionTitleBox {
|
||||
padding: 8px 0;
|
||||
color: rgb(52, 71, 103);
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
opacity: 1;
|
||||
.sectionTitle {
|
||||
margin: 0px;
|
||||
color: rgb(52, 71, 103);
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 1.625;
|
||||
letter-spacing: 0.0075em;
|
||||
text-transform: capitalize;
|
||||
text-decoration: none;
|
||||
vertical-align: unset;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
padding-top: 8px;
|
||||
padding-right: 16px;
|
||||
padding-bottom: 8px;
|
||||
color: rgb(52, 71, 103);
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
opacity: 1;
|
||||
.itemLable {
|
||||
min-width: fit-content;
|
||||
margin: 0px;
|
||||
margin-right: 10px;
|
||||
color: #344767;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0.02857em;
|
||||
text-transform: capitalize;
|
||||
text-decoration: none;
|
||||
vertical-align: unset;
|
||||
opacity: 1;
|
||||
}
|
||||
.itemValue {
|
||||
margin: 0px;
|
||||
color: #7b809a;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0.02857em;
|
||||
text-transform: none;
|
||||
text-decoration: none;
|
||||
vertical-align: unset;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.hr {
|
||||
flex-shrink: 0;
|
||||
margin: 0px;
|
||||
border-color: rgb(242, 244, 247);
|
||||
// border-width: 0px 0px thin;
|
||||
border-style: solid;
|
||||
}
|
||||
.ctrlBox {
|
||||
.ctrlList {
|
||||
position: relative;
|
||||
margin: 0px;
|
||||
padding: 8px 0px;
|
||||
list-style: none;
|
||||
background-color: rgb(249, 250, 251);
|
||||
li {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
box-sizing: border-box;
|
||||
min-width: 0px;
|
||||
margin: 4px;
|
||||
padding: 4px 16px;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
background-color: transparent;
|
||||
border: 0px;
|
||||
border-radius: 0px;
|
||||
outline: 0px;
|
||||
cursor: pointer;
|
||||
transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
|
||||
appearance: none;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-box-align: center;
|
||||
&:hover {
|
||||
color: #3182ce;
|
||||
text-decoration: none;
|
||||
background-color: rgba(16, 24, 40, 0.04);
|
||||
}
|
||||
}
|
||||
.ctrlItemIcon {
|
||||
flex-shrink: 0;
|
||||
min-width: unset;
|
||||
margin-right: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.styles.ctrlItemLable {
|
||||
display: block;
|
||||
margin: 0px;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.settingList {
|
||||
list-style: none;
|
||||
margin: 0px;
|
||||
position: relative;
|
||||
padding: 0px;
|
||||
li {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
background-color: transparent;
|
||||
outline: 0px;
|
||||
border: 0px;
|
||||
margin: 0px;
|
||||
border-radius: 0px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
vertical-align: middle;
|
||||
appearance: none;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
min-width: 0px;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
padding: 8px 16px;
|
||||
transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
|
||||
&.active {
|
||||
background-color: rgba(22, 119, 255, 0.08);
|
||||
.icon {
|
||||
color: rgb(22, 119, 255);
|
||||
}
|
||||
.content {
|
||||
.text {
|
||||
color: rgb(22, 119, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
min-width: 32px;
|
||||
color: #344767;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
}
|
||||
.content {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
.text {
|
||||
margin: 0px;
|
||||
color: #344767;
|
||||
font-size: 16px;
|
||||
// line-height: 1.57;
|
||||
// font-family: var(--tencent-font-family);
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.infoCard {
|
||||
min-height: 100%;
|
||||
background-color: rgb(255, 255, 255);
|
||||
color: rgb(38, 38, 38);
|
||||
transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
|
||||
position: relative;
|
||||
border: 1px solid rgb(230, 235, 241);
|
||||
border-radius: 4px;
|
||||
box-shadow: inherit;
|
||||
.infoCardTitle {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #e6ebf1;
|
||||
padding: 15px 20px;
|
||||
align-items: center;
|
||||
color: rgb(38, 38, 38);
|
||||
margin: 0px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.57;
|
||||
font-family: "tencentFont", sans-serif;
|
||||
}
|
||||
.infoCardContainer {
|
||||
padding: 20px;
|
||||
height: calc(100vh - 260px);
|
||||
overflow: scroll;
|
||||
}
|
||||
.infoCardFooter {
|
||||
border-top: 1px solid #e6ebf1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex: 0 0 auto;
|
||||
padding: 20px;
|
||||
.infoCardFooterContainer {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-flow: wrap;
|
||||
// width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export type MenuItem = {
|
||||
icon: React.ReactNode;
|
||||
key: string;
|
||||
text: string;
|
||||
};
|
||||
@@ -0,0 +1,343 @@
|
||||
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||
import type { Ref } from 'react';
|
||||
import { Button, Form, Input, Select, Row, Col, Space, Tooltip, Switch } from 'antd';
|
||||
import { SENSITIVE_LEVEL_OPTIONS, TAG_DEFINE_TYPE } from '../constant';
|
||||
import { formLayout } from '@/components/FormHelper/utils';
|
||||
import SqlEditor from '@/components/SqlEditor';
|
||||
import { ISemantic } from '../data';
|
||||
import {
|
||||
DIM_OPTIONS,
|
||||
EnumDataSourceType,
|
||||
PARTITION_TIME_FORMATTER,
|
||||
DATE_FORMATTER,
|
||||
} from '@/pages/SemanticModel/Datasource/constants';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
createDimension,
|
||||
updateDimension,
|
||||
mockDimensionAlias,
|
||||
batchCreateTag,
|
||||
batchDeleteTag,
|
||||
} from '../service';
|
||||
import FormItemTitle from '@/components/FormHelper/FormItemTitle';
|
||||
import { message } from 'antd';
|
||||
import { toModelList } from '@/pages/SemanticModel/utils';
|
||||
|
||||
export type CreateFormProps = {
|
||||
modelId: number;
|
||||
domainId: number;
|
||||
dimensionItem?: ISemantic.IDimensionItem;
|
||||
onCancel: () => void;
|
||||
onSubmit?: (values?: any) => void;
|
||||
};
|
||||
|
||||
const FormItem = Form.Item;
|
||||
const { Option } = Select;
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
const DimensionInfoForm: React.FC<CreateFormProps> = forwardRef(
|
||||
(
|
||||
{ modelId, domainId, dimensionItem, onSubmit: handleUpdate }: CreateFormProps,
|
||||
ref: Ref<any>,
|
||||
) => {
|
||||
const isEdit = !!dimensionItem?.id;
|
||||
const [dimensionValueSettingList, setDimensionValueSettingList] = useState<
|
||||
ISemantic.IDimensionValueSettingItem[]
|
||||
>([]);
|
||||
const [form] = Form.useForm();
|
||||
const { setFieldsValue, resetFields } = form;
|
||||
const [llmLoading, setLlmLoading] = useState<boolean>(false);
|
||||
const [formData, setFormData] = useState<ISemantic.IDimensionItem>();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
onSave: () => {
|
||||
return handleSubmit();
|
||||
},
|
||||
}));
|
||||
|
||||
const handleSubmit = async (dimValueMaps?: ISemantic.IDimensionValueSettingItem[]) => {
|
||||
const fieldsValue = await form.validateFields();
|
||||
await saveDimension({
|
||||
...fieldsValue,
|
||||
dimValueMaps: dimValueMaps || dimensionValueSettingList,
|
||||
alias: Array.isArray(fieldsValue.alias) ? fieldsValue.alias.join(',') : '',
|
||||
});
|
||||
};
|
||||
|
||||
const saveDimension = async (fieldsValue: any) => {
|
||||
const queryParams = {
|
||||
modelId: isEdit ? dimensionItem.modelId : modelId,
|
||||
type: 'categorical',
|
||||
...fieldsValue,
|
||||
};
|
||||
let saveDimensionQuery = createDimension;
|
||||
if (queryParams.id) {
|
||||
saveDimensionQuery = updateDimension;
|
||||
}
|
||||
const { code, msg, data } = await saveDimensionQuery(queryParams);
|
||||
if (code === 200) {
|
||||
if (queryParams.isTag) {
|
||||
queryBatchExportTag(data.id || dimensionItem?.id);
|
||||
}
|
||||
if (dimensionItem?.id && !queryParams.isTag) {
|
||||
queryBatchDeleteTag(dimensionItem);
|
||||
}
|
||||
if (!isEdit) {
|
||||
toModelList(domainId, modelId, 'dimension');
|
||||
}
|
||||
message.success('保存维度成功');
|
||||
|
||||
return;
|
||||
}
|
||||
message.error(msg);
|
||||
};
|
||||
|
||||
const queryBatchDeleteTag = async (dimensionItem: ISemantic.IDimensionItem) => {
|
||||
const { code, msg } = await batchDeleteTag([
|
||||
{
|
||||
itemIds: [dimensionItem.id],
|
||||
tagDefineType: TAG_DEFINE_TYPE.DIMENSION,
|
||||
},
|
||||
]);
|
||||
if (code === 200) {
|
||||
return;
|
||||
}
|
||||
message.error(msg);
|
||||
};
|
||||
|
||||
const queryBatchExportTag = async (id: number) => {
|
||||
const { code, msg } = await batchCreateTag([
|
||||
{ itemId: id, tagDefineType: TAG_DEFINE_TYPE.DIMENSION },
|
||||
]);
|
||||
|
||||
if (code === 200) {
|
||||
return;
|
||||
}
|
||||
message.error(msg);
|
||||
};
|
||||
|
||||
const setFormVal = () => {
|
||||
if (dimensionItem) {
|
||||
const { alias } = dimensionItem;
|
||||
const dimensionData = {
|
||||
...dimensionItem,
|
||||
alias: alias && alias.trim() ? alias.split(',') : [],
|
||||
};
|
||||
setFieldsValue(dimensionData);
|
||||
setFormData(dimensionData);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (dimensionItem) {
|
||||
setFormVal();
|
||||
if (Array.isArray(dimensionItem.dimValueMaps)) {
|
||||
setDimensionValueSettingList(dimensionItem.dimValueMaps);
|
||||
} else {
|
||||
setDimensionValueSettingList([]);
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
}
|
||||
}, [dimensionItem]);
|
||||
|
||||
const generatorDimensionAlias = async () => {
|
||||
const fieldsValue = await form.validateFields();
|
||||
setLlmLoading(true);
|
||||
const { code, data } = await mockDimensionAlias({
|
||||
...dimensionItem,
|
||||
...fieldsValue,
|
||||
alias: fieldsValue.alias?.join(','),
|
||||
});
|
||||
setLlmLoading(false);
|
||||
const formAlias = form.getFieldValue('alias');
|
||||
setLlmLoading(false);
|
||||
if (code === 200) {
|
||||
form.setFieldValue('alias', Array.from(new Set([...formAlias, ...data])));
|
||||
} else {
|
||||
message.error('大语言模型解析异常');
|
||||
}
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
return (
|
||||
<>
|
||||
<FormItem hidden={true} name="id" label="ID">
|
||||
<Input placeholder="id" />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="name"
|
||||
label="维度名称"
|
||||
rules={[{ required: true, message: '请输入维度名称' }]}
|
||||
>
|
||||
<Input placeholder="名称不可重复" />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
hidden={isEdit}
|
||||
name="bizName"
|
||||
label="英文名称"
|
||||
rules={[{ required: true, message: '请输入英文名称' }]}
|
||||
>
|
||||
<Input placeholder="名称不可重复" disabled={isEdit} />
|
||||
</FormItem>
|
||||
<FormItem label="别名">
|
||||
<Row>
|
||||
<Col flex="1 1 200px">
|
||||
<FormItem name="alias" noStyle>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder="输入别名后回车确认,多别名输入、复制粘贴支持英文逗号自动分隔"
|
||||
tokenSeparators={[',']}
|
||||
maxTagCount={9}
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
{isEdit && (
|
||||
<Col flex="0 1 75px">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
loading={llmLoading}
|
||||
style={{ top: '2px' }}
|
||||
onClick={() => {
|
||||
generatorDimensionAlias();
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
智能填充
|
||||
<Tooltip title="智能填充将根据维度相关信息,使用大语言模型获取维度别名">
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</Button>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="type"
|
||||
label="类型"
|
||||
rules={[{ required: true, message: '请选择维度类型' }]}
|
||||
>
|
||||
<Select placeholder="请选择维度类型">
|
||||
{DIM_OPTIONS.map((item) => (
|
||||
<Option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</FormItem>
|
||||
{formData?.type &&
|
||||
[EnumDataSourceType.PARTITION_TIME, EnumDataSourceType.TIME].includes(
|
||||
formData.type as EnumDataSourceType,
|
||||
) && (
|
||||
<FormItem
|
||||
name={['ext', 'time_format']}
|
||||
label="时间格式"
|
||||
rules={[{ required: true, message: '请选择时间格式' }]}
|
||||
tooltip="请选择数据库中时间字段对应格式"
|
||||
>
|
||||
<Select placeholder="请选择维度类型">
|
||||
{(formData?.type === EnumDataSourceType.TIME
|
||||
? DATE_FORMATTER
|
||||
: PARTITION_TIME_FORMATTER
|
||||
).map((item) => (
|
||||
<Option key={item} value={item}>
|
||||
{item}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
<FormItem
|
||||
name="semanticType"
|
||||
label="类型"
|
||||
hidden={true}
|
||||
// rules={[{ required: true, message: '请选择维度类型' }]}
|
||||
>
|
||||
<Select placeholder="请选择维度类型">
|
||||
{['CATEGORY', 'ID', 'DATE'].map((item) => (
|
||||
<Option key={item} value={item}>
|
||||
{item}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="sensitiveLevel"
|
||||
label="敏感度"
|
||||
rules={[{ required: true, message: '请选择敏感度' }]}
|
||||
>
|
||||
<Select placeholder="请选择敏感度">
|
||||
{SENSITIVE_LEVEL_OPTIONS.map((item) => (
|
||||
<Option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</FormItem>
|
||||
{/* <FormItem name="commonDimensionId" label="公共维度">
|
||||
<Select placeholder="请绑定公共维度" allowClear options={commonDimensionOptions} />
|
||||
</FormItem> */}
|
||||
{/* <FormItem name="defaultValues" label="默认值">
|
||||
<InfoTagList />
|
||||
</FormItem> */}
|
||||
<Form.Item
|
||||
hidden={!!!process.env.SHOW_TAG}
|
||||
label={
|
||||
<FormItemTitle
|
||||
title={`设为标签`}
|
||||
subTitle={`如果勾选,代表维度的取值都是一种'标签',可用作对实体的圈选`}
|
||||
/>
|
||||
}
|
||||
name="isTag"
|
||||
valuePropName="checked"
|
||||
getValueFromEvent={(value) => {
|
||||
return value === true ? 1 : 0;
|
||||
}}
|
||||
getValueProps={(value) => {
|
||||
return {
|
||||
checked: value === 1,
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<FormItem
|
||||
name="description"
|
||||
label="维度描述"
|
||||
rules={[{ required: true, message: '请输入维度描述' }]}
|
||||
>
|
||||
<TextArea placeholder="请输入维度描述" />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="expr"
|
||||
label="表达式"
|
||||
tooltip="表达式中的字段必须在创建模型的时候被标记为日期或者维度"
|
||||
rules={[{ required: true, message: '请输入表达式' }]}
|
||||
>
|
||||
<SqlEditor height={'150px'} />
|
||||
</FormItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form
|
||||
{...formLayout}
|
||||
form={form}
|
||||
onValuesChange={(value, values) => {
|
||||
setFormData(values);
|
||||
}}
|
||||
>
|
||||
{renderContent()}
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default DimensionInfoForm;
|
||||
@@ -11,6 +11,7 @@ import TableHeaderFilter from '@/components/TableHeaderFilter';
|
||||
import moment from 'moment';
|
||||
import styles from './style.less';
|
||||
import { ISemantic } from '../data';
|
||||
import { toModelList } from '@/pages/SemanticModel/utils';
|
||||
|
||||
type Props = {
|
||||
disabledEdit?: boolean;
|
||||
@@ -105,9 +106,7 @@ const ModelTable: React.FC<Props> = ({ modelList, disabledEdit = false, onModelC
|
||||
<a
|
||||
onClick={() => {
|
||||
setSelectModel(record);
|
||||
|
||||
history.push(`/model/domain/manager/${domainId}/${id}`);
|
||||
// onModelChange?.(record);
|
||||
toModelList(domainId, id);
|
||||
}}
|
||||
>
|
||||
{_}
|
||||
|
||||
@@ -13,7 +13,7 @@ import IndicatorStar, { StarType } from '../components/IndicatorStar';
|
||||
interface IndicatorInfo {
|
||||
url?: string;
|
||||
starType?: StarType;
|
||||
onNameClick?: (record: ISemantic.IMetricItem) => void | boolean;
|
||||
onNameClick?: (record: ISemantic.IMetricItem | ISemantic.IDimensionItem) => void | boolean;
|
||||
}
|
||||
|
||||
interface ColumnsConfigParams {
|
||||
@@ -116,18 +116,43 @@ export const ColumnsConfig = (params?: ColumnsConfigParams) => {
|
||||
},
|
||||
dimensionInfo: {
|
||||
render: (_, record: ISemantic.IDimensionItem) => {
|
||||
const { name, alias, bizName } = record;
|
||||
const { name, alias, bizName, id, domainId, modelId } = record;
|
||||
let url = `/demension/detail/${id}`;
|
||||
if (params?.indicatorInfo) {
|
||||
url = replaceRouteParams(params.indicatorInfo.url || '', {
|
||||
domainId: `${domainId}`,
|
||||
modelId: `${modelId}`,
|
||||
indicatorId: `${id}`,
|
||||
});
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Space>
|
||||
<span style={{ fontWeight: 500 }}>{name}</span>
|
||||
<a
|
||||
className={styles.textLink}
|
||||
style={{ fontWeight: 500 }}
|
||||
onClick={(event: any) => {
|
||||
if (params?.indicatorInfo?.onNameClick) {
|
||||
const state = params.indicatorInfo.onNameClick(record);
|
||||
if (state === false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
history.push(url);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</a>
|
||||
{/* <span style={{ fontWeight: 500 }}>{name}</span> */}
|
||||
</Space>
|
||||
</div>
|
||||
<div style={{ color: '#5f748d', fontSize: 14, marginTop: 5, marginLeft: 0 }}>
|
||||
{bizName}
|
||||
</div>
|
||||
{renderAliasAndClassifications(alias, undefined)}
|
||||
{alias && renderAliasAndClassifications(alias, undefined)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
@@ -136,7 +161,7 @@ export const ColumnsConfig = (params?: ColumnsConfigParams) => {
|
||||
render: (_, record: ISemantic.IMetricItem) => {
|
||||
const { name, alias, bizName, classifications, id, isCollect, domainId, modelId } = record;
|
||||
|
||||
let url = `/metric/detail/`;
|
||||
let url = `/metric/detail/${id}`;
|
||||
let starType: StarType = 'metric';
|
||||
if (params?.indicatorInfo) {
|
||||
url = replaceRouteParams(params.indicatorInfo.url || '', {
|
||||
@@ -174,7 +199,7 @@ export const ColumnsConfig = (params?: ColumnsConfigParams) => {
|
||||
<div style={{ color: '#5f748d', fontSize: 14, marginTop: 5, marginLeft: 0 }}>
|
||||
{bizName}
|
||||
</div>
|
||||
{renderAliasAndClassifications(alias, classifications)}
|
||||
{alias && renderAliasAndClassifications(alias, classifications)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -419,7 +419,7 @@
|
||||
.infoCardTitle {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #e6ebf1;
|
||||
padding: 20px 20px 20px 40px;
|
||||
padding: 15px 20px;
|
||||
align-items: center;
|
||||
color: rgb(38, 38, 38);
|
||||
margin: 0px;
|
||||
|
||||
Reference in New Issue
Block a user