diff --git a/webapp/packages/supersonic-fe/package.json b/webapp/packages/supersonic-fe/package.json index dfa6ecdcb..2eb34d4d8 100644 --- a/webapp/packages/supersonic-fe/package.json +++ b/webapp/packages/supersonic-fe/package.json @@ -64,7 +64,9 @@ "@antv/g6": "^4.8.23", "@antv/g6-core": "^0.8.23", "@antv/layout": "^0.3.20", + "@antv/x6": "1.30.1", "@antv/xflow": "^1.0.55", + "@antv/xflow-extension": "1.0.55", "@babel/runtime": "^7.22.5", "@types/numeral": "^2.0.2", "@types/react-draft-wysiwyg": "^1.13.2", diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/components/DataSourceCreateForm.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/components/DataSourceCreateForm.tsx index 410276dd2..f1a725dff 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/components/DataSourceCreateForm.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Datasource/components/DataSourceCreateForm.tsx @@ -5,7 +5,13 @@ import DataSourceFieldForm from './DataSourceFieldForm'; import { formLayout } from '@/components/FormHelper/utils'; import { EnumDataSourceType } from '../constants'; import styles from '../style.less'; -import { updateModel, createModel, getColumns, getUnAvailableItem } from '../../service'; +import { + updateModel, + createModel, + getColumns, + getUnAvailableItem, + getTagObjectList, +} from '../../service'; import type { Dispatch } from 'umi'; import type { StateType } from '../../model'; import { connect } from 'umi'; @@ -63,7 +69,8 @@ const DataSourceCreateForm: React.FC = ({ const [effectTipsData, setEffectTipsData] = useState< (ISemantic.IDimensionItem | ISemantic.IMetricItem)[] >([]); - + const [tagObjectList, setTagObjectList] = useState([]); + const [tagObjectIdState, setTagObjectIdState] = useState(modelItem?.tagObjectId); const formValRef = useRef(initFormVal as any); const [form] = Form.useForm(); const { databaseConfigList, selectModelId: modelId, selectDomainId } = domainManger; @@ -91,9 +98,23 @@ const DataSourceCreateForm: React.FC = ({ } }, [scriptColumns]); + useEffect(() => { + queryTagObjectList(); + }, []); + const forward = () => setCurrentStep(currentStep + 1); const backward = () => setCurrentStep(currentStep - 1); + const queryTagObjectList = async () => { + const { code, msg, data } = await getTagObjectList({ domainId: selectDomainId }); + if (code === 200) { + setTagObjectList(data); + + return; + } + message.error(msg); + }; + const checkAvailableItem = async (fields: string[] = []) => { if (!modelItem) { return false; @@ -151,7 +172,7 @@ const DataSourceCreateForm: React.FC = ({ (fieldsClassify, item: any) => { const { type, - bizName, + // bizName, fieldName, timeGranularity, agg, @@ -159,7 +180,7 @@ const DataSourceCreateForm: React.FC = ({ name, isCreateMetric: createMetric, dateFormat, - entityNames, + // entityNames, isTag, } = item; const isCreateDimension = createDimension ? 1 : 0; @@ -194,7 +215,8 @@ const DataSourceCreateForm: React.FC = ({ isCreateDimension, name, type, - entityNames, + // entityNames, + tagObjectId: modelItem?.tagObjectId, }); break; case EnumDataSourceType.MEASURES: @@ -253,6 +275,7 @@ const DataSourceCreateForm: React.FC = ({ sqlQuery: sql, sqlVariables: sqlParams, }, + tagObjectId: tagObjectIdState, }; setQueryParamsState(queryParams); const checkState = await checkAvailableItem(fieldColumns.map((item) => item.nameEn)); @@ -370,6 +393,7 @@ const DataSourceCreateForm: React.FC = ({ initFields([], fieldColumns); } } + setTagObjectIdState(modelItem?.tagObjectId); }, [modelItem]); useEffect(() => { @@ -423,6 +447,11 @@ const DataSourceCreateForm: React.FC = ({
{ + setTagObjectIdState(tagObjectId); + }} onFieldChange={handleFieldChange} onSqlChange={(sql) => { setSqlFilter(sql); 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 e0cb1f698..599aca339 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 @@ -4,6 +4,7 @@ import TableTitleTooltips from '../../components/TableTitleTooltips'; import { isUndefined } from 'lodash'; import { ExclamationCircleOutlined } from '@ant-design/icons'; import SqlEditor from '@/components/SqlEditor'; +import { ISemantic } from '../../data'; import { TYPE_OPTIONS, DATE_FORMATTER, @@ -23,7 +24,8 @@ type FieldItem = { checked?: number; dateFormat?: string; timeGranularity?: string; - entityNames?: string[]; + // entityNames?: string[]; + // tagObjectId?: number; isTag?: number; }; const { Search } = Input; @@ -32,8 +34,11 @@ const FormItem = Form.Item; type Props = { onSqlChange: (sql: string) => void; sql: string; + tagObjectList: ISemantic.ITagObjectItem[]; + tagObjectId?: number; fields: FieldItem[]; onFieldChange: (fieldName: string, data: Partial) => void; + onTagObjectChange?: (tagObjectId: number) => void; }; const { Option } = Select; @@ -47,7 +52,15 @@ const getCreateFieldName = (type: EnumDataSourceType) => { return isCreateName; }; -const DataSourceFieldForm: React.FC = ({ fields, sql, onFieldChange, onSqlChange }) => { +const DataSourceFieldForm: React.FC = ({ + fields, + sql, + tagObjectList, + tagObjectId, + onTagObjectChange, + onFieldChange, + onSqlChange, +}) => { const handleFieldChange = (record: FieldItem, fieldName: string, value: any) => { onFieldChange(record.bizName, { ...record, @@ -123,12 +136,28 @@ const DataSourceFieldForm: React.FC = ({ fields, sql, onFieldChange, onSq width: 185, render: (_: any, record: FieldItem) => { const { type } = record; + console.log(record, 3333); if (type === EnumDataSourceType.PRIMARY) { - const entityNames = - fields.find((field) => field.bizName === record.bizName)?.entityNames || []; return ( + {/* */} = ({ fields, sql, onFieldChange, onSq /> - + */} ); } diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/CreateNodeModal/index.less b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/CreateNodeModal/index.less new file mode 100644 index 000000000..58e2c91b1 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/CreateNodeModal/index.less @@ -0,0 +1 @@ +.create-entity-container {} diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/CreateNodeModal/index.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/CreateNodeModal/index.tsx new file mode 100644 index 000000000..67cb0cb3a --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/CreateNodeModal/index.tsx @@ -0,0 +1,148 @@ +import React from 'react' +import { Modal, Form, Input, Radio, Select } from 'antd' +import _ from 'lodash' +import { EntityType, EntityTypeDisplay } from '../const' +import './index.less'; + +const formItemLayout = { + labelCol: { + xs: { span: 24 }, + sm: { span: 6 }, + }, + wrapperCol: { + xs: { span: 24 }, + sm: { span: 16 }, + }, +}; + +interface Props { + visible: boolean; + onOk: Function; + onCancel: Function; +} + +/** 创建模型弹窗 */ +const CreateEntityModal = (props: Props) => { + const { visible, onOk, onCancel } = props + const [confirmLoading, setConfirmLoading] = React.useState(false) + const [currentEntityType, setCurrentEntityType] = React.useState(EntityType.FACT) + const [form] = Form.useForm() + + const hanldeOk = () => { + form.validateFields().then(values => { + const callback = (result: any) => { + setConfirmLoading(false) + if (result) { + onCancel(); + } + } + setConfirmLoading(true) + onOk({ + ...values, + cb: callback, + }) + }) + }; + + const onChange = (e: any) => { + /** 切换模型类型重置表单 */ + form.resetFields(); + setCurrentEntityType(e.target.value) + }; + + return ( + onCancel()} + mask={false} + centered + destroyOnClose={true} + > +
+ + + {_.map(EntityType, (type: EntityType) => { + return ( + + {EntityTypeDisplay[type]} + + ); + })} + + + { + if (!v) { + callback('请输入中文名称'); + } + const reg1 = new RegExp(`^[a-zA-Z0-9_]*$`); + if (reg1.test(v)) { + callback('必须包含中文'); + } + const reg2 = new RegExp('^[\\u4e00-\\u9fa5a-zA-Z0-9_]*$'); + if (reg2.test(v)) { + callback(); + } else { + callback('只能包含中文、字符、数字、下划线'); + } + }, + }, + ] + } + initialValue={'用户创建的表'} + > + + + { + if (!v) { + callback('请输入英文名'); + } else if (v.includes(' ')) { + callback('不能包含空格'); + } + const reg = new RegExp(`^[a-zA-Z0-9_]*$`); + if (reg.test(v)) { + callback(); + } else { + callback('只能包含数字、字符、下划线'); + } + }, + }, + ] + } + initialValue={'customNode'} + > + + +
+
+ ); +} + +export default CreateEntityModal diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/CreateRelationModal/index.less b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/CreateRelationModal/index.less new file mode 100644 index 000000000..99d13aa1c --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/CreateRelationModal/index.less @@ -0,0 +1 @@ +.create-relation-container {} diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/CreateRelationModal/index.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/CreateRelationModal/index.tsx new file mode 100644 index 000000000..a85fa2675 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/CreateRelationModal/index.tsx @@ -0,0 +1,89 @@ +import React, { useState } from 'react' +import { Modal, Form, Input, Select } from 'antd' +import type { EntityCanvasModel } from '../interface' + +const formItemLayout = { + labelCol: { + xs: { span: 24 }, + sm: { span: 6 }, + }, + wrapperCol: { + xs: { span: 24 }, + sm: { span: 16 }, + }, +} + +interface Props { + visible: boolean + onOk: (value: any) => void + onCancel: () => void + sourceEntity?: EntityCanvasModel + targetEntity?: EntityCanvasModel +} + +const CreateRelationModal = (props: Props) => { + const { visible, sourceEntity, targetEntity, onOk, onCancel } = props + const [confirmLoading, setConfirmLoading] = useState(false) + const [form] = Form.useForm() + + const handleOK = () => { + form.validateFields().then(values => { + setConfirmLoading(true) + const cb = () => { + setConfirmLoading(false) + } + onOk({ ...values, cb }) + }) + } + + return ( + +
+ + + + + + + + + +
+
+ ) +} + +export default CreateRelationModal diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/GraphToolbar/index.less b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/GraphToolbar/index.less new file mode 100644 index 000000000..930580a4e --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/GraphToolbar/index.less @@ -0,0 +1,20 @@ +.xflow-er-solution-toolbar { + display: flex; + align-items: center; + width: 100%; + height: 40px; + background-color: #ced4de; + + .icon { + padding: 0 8px; + } + + .disabled { + cursor: not-allowed; + color: rgba(0, 0, 0, 0.3) + } + + .icon:hover { + color: #000; + } +} diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/GraphToolbar/index.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/GraphToolbar/index.tsx new file mode 100644 index 000000000..6bdb18aa8 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/GraphToolbar/index.tsx @@ -0,0 +1,53 @@ +import React from 'react' +import {} from 'antd' +import { PlusCircleOutlined, DeleteOutlined, LinkOutlined } from '@ant-design/icons' +import { MODELS, useXFlowApp } from '@antv/xflow' +import './index.less' + +interface Props { + onAddNodeClick: () => void + onDeleteNodeClick: () => void + onConnectEdgeClick: () => void +} + +const GraphToolbar = (props: Props) => { + const { onAddNodeClick, onDeleteNodeClick, onConnectEdgeClick } = props + const [selectedNodes, setSelectedNodes] = React.useState([]) + + /** 监听画布中选中的节点 */ + const watchModelService = async () => { + const appRef = useXFlowApp() + const modelService = appRef && appRef?.modelService + if (modelService) { + const model = await MODELS.SELECTED_NODES.getModel(modelService) + model.watch(async () => { + const nodes = await MODELS.SELECTED_NODES.useValue(modelService) + setSelectedNodes(nodes) + }) + } + } + + watchModelService() + + return ( +
+
onAddNodeClick()}> + 添加节点 + +
+
onConnectEdgeClick()}> + 添加关系 + +
+
0 ? '' : 'disabled'}`} + onClick={() => onDeleteNodeClick()} + > + 删除节点 + +
+
+ ) +} + +export default GraphToolbar diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/config-cmd.ts b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/config-cmd.ts new file mode 100644 index 000000000..1f38d8ee8 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/config-cmd.ts @@ -0,0 +1,25 @@ +import { createCmdConfig, DisposableCollection } from '@antv/xflow' +import { MockApi } from './service' + +export const useCmdConfig = createCmdConfig(config => { + /** 设置hook */ + config.setRegisterHookFn(hooks => { + const list = [ + hooks.addNode.registerHook({ + name: 'addNodeHook', + handler: async args => { + args.createNodeService = MockApi.addNode + }, + }), + hooks.addEdge.registerHook({ + name: 'addEdgeHook', + handler: async args => { + args.createEdgeService = MockApi.addEdge + }, + }), + ] + const toDispose = new DisposableCollection() + toDispose.pushAll(list) + return toDispose + }) +}) diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/config-graph.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/config-graph.tsx new file mode 100644 index 000000000..b8fbb7fa1 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/config-graph.tsx @@ -0,0 +1,32 @@ +import { createGraphConfig } from '@antv/xflow' + +export const useGraphConfig = createGraphConfig(config => { + /** 预设XFlow画布配置项 */ + config.setX6Config({ + grid: true, + scroller: { + enabled: true, + }, + scaling: { + min: 0.2, + max: 3, + }, + connecting: { + /** 连线过程中距离目标节点50px时自动吸附 */ + snap: { + radius: 50, + }, + connector: { + name: 'rounded', + args: { + radius: 50, + }, + }, + router: { + name: 'er', + }, + /** 不允许连接到画布空白位置, 即没有目标节点时连线会自动消失 */ + allowBlank: false, + }, + }) +}) diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/config-keybinding.ts b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/config-keybinding.ts new file mode 100644 index 000000000..a8f3d8381 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/config-keybinding.ts @@ -0,0 +1,80 @@ +import type { NsNodeCmd, NsEdgeCmd, IGraphCommandService } from '@antv/xflow' +import { createKeybindingConfig, XFlowNodeCommands, XFlowEdgeCommands, MODELS } from '@antv/xflow' +import type { Node as X6Node, Edge as X6Edge } from '@antv/x6' +import { Platform } from '@antv/x6' +import { message } from 'antd' + +/** 快捷键 */ +enum ShortCut { + DELETE = 'Backspace', // 删除 + CmdDelete = 'Cmd+Delete', // Mac按住Command多选删除 + CtrlDelete = 'Ctrl+Delete', // Windows按住Ctrl多选删除 +} + +export const useKeybindingConfig = createKeybindingConfig(config => { + config.setKeybindingFunc(registry => { + return registry.registerKeybinding([ + { + id: 'delete', + keybinding: ShortCut.DELETE, + callback: async (item, modelService, commandService, e) => { + /** 如果是input的delete事件, 则不走删除的回调 */ + const target = e && (e?.target as HTMLElement) + if (target && target.tagName && target.tagName.toLowerCase() === 'input') { + return + } + const cells = await MODELS.SELECTED_CELLS.useValue(modelService) + const nodes = cells?.filter(cell => cell.isNode()) + const edges = cells?.filter(cell => cell.isEdge()) + if (edges?.length > 0) { + deleteEdges(commandService, edges as X6Edge[]) + } + if (nodes?.length > 0) { + deleteNodes(commandService, nodes as X6Node[]) + } + }, + }, + { + id: 'deleteAll', + keybinding: Platform.IS_MAC ? ShortCut.CmdDelete : ShortCut.CtrlDelete, + callback: async (item, modelService, commandService, e) => { + const cells = await MODELS.SELECTED_CELLS.useValue(modelService) + const nodes = cells?.filter(cell => cell.isNode()) + const edges = cells?.filter(cell => cell.isEdge()) + deleteEdges(commandService, edges as X6Edge[]) + deleteNodes(commandService, nodes as X6Node[]) + }, + }, + ]) + }) +}) + +export const deleteNodes = async (commandService: IGraphCommandService, nodes: X6Node[]) => { + const promiseList = nodes?.map(node => { + return commandService.executeCommand(XFlowNodeCommands.DEL_NODE.id, { + nodeConfig: { + ...node.getData(), + }, + } as NsNodeCmd.DelNode.IArgs) + }) + const res = await Promise.all(promiseList) + if (res.length > 0) { + message.success('删除节点成功!') + } +} + +export const deleteEdges = async (commandServce: IGraphCommandService, edges: X6Edge[]) => { + const promiseList = edges + ?.filter(edge => edge.isEdge()) + ?.map(edge => { + return commandServce.executeCommand(XFlowEdgeCommands.DEL_EDGE.id, { + edgeConfig: { + ...edge.getData(), + }, + } as NsEdgeCmd.DelEdge.IArgs) + }) + const res = await Promise.all(promiseList) + if (res.length > 0) { + message.success('删除连线成功!') + } +} diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/const.ts b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/const.ts new file mode 100644 index 000000000..52980f811 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/const.ts @@ -0,0 +1,16 @@ +export enum GraphMode { + INFO = 'INFO', // 缩略模式 + DETAIL = 'DETAIL', // 详情模式 +} + +export enum EntityType { + FACT = 'FACT', + DIM = 'DIM', + OTHER = 'OTHER', +} + +export const EntityTypeDisplay = { + [EntityType.FACT]: '事实表', + [EntityType.DIM]: '维度表', + [EntityType.OTHER]: '其他表', +} diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/index.less b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/index.less new file mode 100644 index 000000000..a011a5df0 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/index.less @@ -0,0 +1,57 @@ +// @import '@antv/xflow/dist/index.css'; + +.xflow-er-solution-container { + width: 100%; + height: 550px; + border: 1px solid #ebedf1; + background-color: #fff; + + .cursor-tip-container { + position: fixed; + top: 0; + left: 0; + z-index: 999; + display: none; + width: 200px; + height: 40px; + color: #000; + background: #ced4de; + + .draft-entity-container { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + padding-left: 8px; + } + } + + .xflow-canvas-root { + margin-top: 40px; + } + + /** 覆盖节点默认选中色 */ + .x6-node-selected rect { + stroke: #1890ff; + stroke-width: 4px; + } + + .x6-edge-selected path { + stroke: #1890ff; + stroke-width: 2px; + } + + /** 默认隐藏链接桩 */ + .x6-port-body { + z-index: 1; + visibility: hidden; + } +} + +.er-demo { + .__dumi-default-previewer-actions { + border: 0; + } +} diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/index.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/index.tsx new file mode 100644 index 000000000..f7414e9bd --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/index.tsx @@ -0,0 +1,314 @@ +import React, { useState } from 'react'; +import type { IAppLoad, NsGraph, IApplication } from '@antv/xflow'; +import { XFlow, XFlowCanvas, KeyBindings } from '@antv/xflow'; +import { XFlowAppProvider, useXFlowApp } from '@antv/xflow'; +import type { NsGraphCmd, NsNodeCmd, NsEdgeCmd } from '@antv/xflow'; +import { XFlowGraphCommands, XFlowNodeCommands, XFlowEdgeCommands } from '@antv/xflow'; +import { CanvasMiniMap, CanvasScaleToolbar, CanvasSnapline } from '@antv/xflow'; +import { MODELS } from '@antv/xflow'; +import GraphToolbar from './GraphToolbar/index'; +import { connect } from 'umi'; + +/** 配置画布 */ +import { useGraphConfig } from './config-graph'; +/** 配置Command */ +import { useCmdConfig } from './config-cmd'; +/** 配置快捷键 */ +import { useKeybindingConfig } from './config-keybinding'; + +import { message } from 'antd'; +import type { EntityCanvasModel } from './interface'; + +import CreateNodeModal from './CreateNodeModal'; +import CreateRelationModal from './CreateRelationModal'; +import Entity from './react-node/Entity'; +import Relation from './react-edge/Relation'; + +import '@antv/xflow/dist/index.css'; +import './index.less'; + +/** Mock所有与服务端交互的接口 */ +import { MockApi } from './service'; + +type Props = { + domainManger: StateType; + dispatch: Dispatch; +}; + +/** 鼠标的引用 */ +let cursorTipRef: HTMLDivElement; +/** 鼠标在画布的位置 */ +let cursorLocation: any; + +const DomainManger: React.FC = (demoProps: Props) => { + /** XFlow应用实例 */ + const app = useXFlowApp(); + + /** 画布配置项 */ + const graphConfig = useGraphConfig(); + /** 预设XFlow画布需要渲染的React节点 / 边 */ + graphConfig.setNodeRender('NODE1', (props) => { + return ; + }); + graphConfig.setEdgeRender('EDGE1', (props) => { + return ; + }); + /** 命令配置项 */ + const cmdConfig = useCmdConfig(); + /** 快捷键配置项 */ + const keybindingConfig = useKeybindingConfig(); + + /** 是否画布处于可以新建节点状态 */ + const [graphStatuts, setGraphStatus] = useState('NORMAL'); + /** 是否展示新建节点弹窗 */ + const [isShowCreateNodeModal, setIsShowCreateNodeModal] = useState(false); + /** 是否展示新建关联关系弹窗 */ + const [isShowCreateRelationModal, setIsShowCreateRelationModal] = useState(false); + /** 连线source数据 */ + const [relationSourceData, setRelationSourceData] = useState(undefined); + /** 连线target数据 */ + const [relationTargetData, setRelationTargetData] = useState(undefined); + + /** XFlow初始化完成的回调 */ + const onLoad: IAppLoad = async (app) => { + const graph = await app.getGraphInstance(); + graph.zoom(-0.2); + + /** Mock从服务端获取数据 */ + const graphData = await MockApi.loadGraphData(); + /** 渲染画布数据 */ + await app.executeCommand(XFlowGraphCommands.GRAPH_RENDER.id, { + graphData, + } as NsGraphCmd.GraphRender.IArgs); + + /** 设置监听事件 */ + await watchEvent(app); + }; + + /** 监听事件 */ + const watchEvent = async (appRef: IApplication) => { + if (appRef) { + const graph = await appRef.getGraphInstance(); + graph.on('node:mousedown', ({ e, x, y, node, view }) => { + appRef.executeCommand(XFlowNodeCommands.FRONT_NODE.id, { + nodeId: node?.getData()?.id, + } as NsNodeCmd.FrontNode.IArgs); + }); + graph.on('edge:connected', ({ edge }) => { + const relationSourceData = edge?.getSourceNode()?.data; + const relationTargetData = edge?.getTargetNode()?.data; + setRelationSourceData(relationSourceData); + setRelationTargetData(relationTargetData); + setIsShowCreateRelationModal(true); + /** 拖拽过程中会生成一条无实际业务含义的线, 需要手动删除 */ + const edgeData: NsGraph.IEdgeConfig = edge?.getData(); + if (!edgeData) { + appRef.executeCommand(XFlowEdgeCommands.DEL_EDGE.id, { + x6Edge: edge as any, + } as NsEdgeCmd.DelEdge.IArgs); + } + }); + graph.on('node:mouseenter', ({ e, node, view }) => { + changePortsVisible(true); + }); + graph.on('node:mouseleave', ({ e, node, view }) => { + changePortsVisible(false); + }); + graph.on('edge:click', ({ edge }) => { + edge.toFront(); + }); + } + }; + + const changePortsVisible = (visible: boolean) => { + const ports = document.body.querySelectorAll('.x6-port-body') as NodeListOf; + for (let i = 0, len = ports.length; i < len; i = i + 1) { + ports[i].style.visibility = visible ? 'visible' : 'hidden'; + } + }; + + /** 创建画布节点 */ + const handleCreateNode = async (values: any) => { + const { cb, ...rest } = values; + + const graph = await app.getGraphInstance(); + /** div块鼠标的位置转换为画布的位置 */ + const graphLoc = graph.clientToLocal(cursorLocation.x, cursorLocation.y - 200); + + const res = await app.executeCommand(XFlowNodeCommands.ADD_NODE.id, { + nodeConfig: { + id: 'customNode', + x: graphLoc.x, + y: graphLoc.y, + width: 214, + height: 252, + renderKey: 'NODE1', + entityId: values?.name, + entityName: values?.displayName, + entityType: 'FACT', + }, + } as NsNodeCmd.AddNode.IArgs); + + if (res) { + cb && cb(); + setIsShowCreateNodeModal(false); + message.success('添加节点成功!'); + } + }; + + /** 删除画布节点 */ + const handleDeleteNode = async (nodeId: string) => { + const res = await app.executeCommand(XFlowNodeCommands.DEL_NODE.id, { + nodeConfig: { id: nodeId }, + } as NsNodeCmd.DelNode.IArgs); + + if (res) { + message.success('删除节点成功!'); + } + }; + + /** 创建关联关系 */ + const handleCreateEdge = async (values: any) => { + const { cb, ...rest } = values; + const res = await app.executeCommand(XFlowEdgeCommands.ADD_EDGE.id, { + edgeConfig: { + id: 'fact1-other2', + source: 'fact1', + target: 'other2', + renderKey: 'EDGE1', + edgeContentWidth: 20, + edgeContentHeight: 20, + /** 设置连线样式 */ + attrs: { + line: { + stroke: '#d8d8d8', + strokeWidth: 1, + targetMarker: { + name: 'classic', + }, + }, + }, + }, + } as NsEdgeCmd.AddEdge.IArgs); + + if (res) { + cb && cb(); + setIsShowCreateRelationModal(false); + message.success('添加连线成功!'); + } + }; + + /** 删除关联关系 */ + const handleDeleteEdge = async (relationId: string) => { + const res = await app.executeCommand(XFlowEdgeCommands.DEL_EDGE.id, { + edgeConfig: { id: relationId }, + } as NsEdgeCmd.DelEdge.IArgs); + if (res) { + message.success('删除连线成功!'); + } + }; + + /** 设置鼠标样式 */ + const configCursorTip = ({ left, top, display }) => { + cursorTipRef.style.left = left; + cursorTipRef.style.top = top; + cursorTipRef.style.display = display; + }; + + return ( + +
{ + if (graphStatuts === 'CREATE') { + configCursorTip({ + left: `${e.pageX}px`, + top: `${e.pageY - 180}px`, + display: 'block', + }); + } + }} + onMouseDown={(e) => { + if (graphStatuts === 'CREATE') { + cursorLocation = { x: e.pageX, y: e.pageY }; + setIsShowCreateNodeModal(true); + configCursorTip({ + left: '0px', + top: '0px', + display: 'none', + }); + setGraphStatus('NORMAL'); + } + }} + onMouseLeave={(e) => { + if (graphStatuts === 'CREATE') { + configCursorTip({ + left: '0px', + top: '0px', + display: 'none', + }); + } + }} + > + + { + message.info('鼠标移动到画布空白位置, 再次点击鼠标完成创建', 2); + setGraphStatus('CREATE'); + }} + onDeleteNodeClick={async () => { + const modelService = app.modelService; + const nodes = await MODELS.SELECTED_NODES.useValue(modelService); + nodes.forEach((node) => { + handleDeleteNode(node?.id); + }); + }} + onConnectEdgeClick={() => { + setIsShowCreateRelationModal(true); + }} + /> + + + + + + + {/** 占位空节点 */} + {graphStatuts === 'CREATE' && ( +
{ + ref && (cursorTipRef = ref); + }} + > +
+
未命名模型
+
+
+ )} + {/** 创建节点弹窗 */} + { + setIsShowCreateNodeModal(false); + }} + /> + {/** 创建关联关系弹窗 */} + { + setIsShowCreateRelationModal(false); + }} + /> +
+
+
+ ); +}; + +export default connect(({ domainManger }: { domainManger: StateType }) => ({ + domainManger, +}))(DomainManger); diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/interface.ts b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/interface.ts new file mode 100644 index 000000000..93354c6b3 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/interface.ts @@ -0,0 +1,33 @@ +import type { NsGraph } from '@antv/xflow' + +/** 实体数据模型 */ +export interface EntityModel { + /** 实体id */ + entityId: string + /** 实体name */ + entityName: string + /** 实体类型 */ + entityType: string + /** 实体的属性字段 */ + properties: EntityProperty[] +} + +/** 属性字段数据模型 */ +export interface EntityProperty { + /** 属性id */ + propertyId: string + /** 属性名称 */ + propertyName: string + /** 属性类型 */ + propertyType: string + /** 是否主键 */ + isPK?: boolean + /** 是否外键 */ + isFK?: boolean +} + +/** 画布实体渲染模型 */ +export interface EntityCanvasModel extends EntityModel, NsGraph.INodeConfig {} + +/** 画布连线渲染模型 */ +export type RelationCanvasModel = NsGraph.IEdgeConfig diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/mock.ts b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/mock.ts new file mode 100644 index 000000000..141485397 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/mock.ts @@ -0,0 +1,127 @@ +import type { EntityProperty, EntityCanvasModel, RelationCanvasModel } from './interface'; + +export const mockProperties: EntityProperty[] = [ + { + propertyId: 'propertyId1', + propertyName: '业务日期', + propertyType: 'string', + isPK: true, + }, + { + propertyId: 'propertyId2', + propertyName: '交易号1', + propertyType: 'bigint', + isFK: true, + }, + { + propertyId: 'propertyId3', + propertyName: '最长显示的表单名最长显示的表单名', + propertyType: 'string', + }, + { + propertyId: 'propertyId4', + propertyName: '交易支付外键', + propertyType: 'string', + }, + { + propertyId: 'propertyId5', + propertyName: '卖家支付日期', + propertyType: 'string', + }, + { + propertyId: 'propertyId6', + propertyName: '网商银行', + propertyType: 'string', + }, + { + propertyId: 'propertyId7', + propertyName: '业务日期', + propertyType: 'string', + }, + { + propertyId: 'propertyId8', + propertyName: '业务日期111', + propertyType: 'string', + }, + { + propertyId: 'propertyId9', + propertyName: '业务日期222', + propertyType: 'string', + }, + { + propertyId: 'propertyId10', + propertyName: '业务日期333', + propertyType: 'string', + }, +]; + +export const mockEntityData: EntityCanvasModel[] = [ + { + id: 'fact1', + x: 450, + y: 150, + width: 214, + height: 252, + entityId: 'fact1', + entityName: '模型', + entityType: 'FACT', + properties: mockProperties, + }, + { + id: 'fact2', + x: 0, + y: -20, + width: 214, + height: 252, + entityId: 'fact2', + entityName: '事实表2', + entityType: 'FACT', + properties: mockProperties, + }, + { + id: 'dim1', + x: 0, + y: 300, + width: 214, + height: 252, + entityId: 'dim1', + entityName: '维度表1', + entityType: 'DIM', + properties: mockProperties, + }, + { + id: 'other1', + x: 180, + y: 500, + width: 214, + height: 252, + entityId: 'other1', + entityName: '其他表1', + entityType: 'OTHER', + properties: mockProperties, + }, + { + id: 'other2', + x: 810, + y: 0, + width: 214, + height: 252, + entityId: 'other2', + entityName: '其他表2', + entityType: 'OTHER', + properties: mockProperties, + }, +]; + +export const mockRelationData: RelationCanvasModel[] = [ + { + id: 'fact1-fact2', + source: 'fact1', + target: 'fact2', + }, + { + id: 'fact1-dim1', + source: 'fact1', + target: 'dim1', + }, +]; diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/react-edge/Relation.less b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/react-edge/Relation.less new file mode 100644 index 000000000..6ecf39e3f --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/react-edge/Relation.less @@ -0,0 +1,74 @@ +.relation-count-container { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + color: #868c91; + background-color: #d8d8d8; + cursor: pointer; + &:hover { + color: #fff; + background-color: #1890ff; + } +} + +.relation-operation-popover .ant-popover-inner-content { + padding: 0; +} + +.relation-operation-container { + width: 220px; + max-height: 80px; + padding: 12px 16px; + overflow: hidden; + background-color: #fff; + + &:hover { + overflow-y: auto; + } + + &::-webkit-scrollbar { + width: 5px; + background: #2b2f33; + } + + &::-webkit-scrollbar-thumb { + background: #5f656b; + border-radius: 10px; + } + + .relation-operation-item { + display: flex; + align-items: center; + justify-content: space-between; + height: 28px; + color: #000; + .relation-operation-item-content { + display: flex; + flex-basis: 160px; + align-items: center; + justify-content: space-between; + height: 100%; + + // &:hover { + // cursor: pointer; + // background: #d8d8d8 + // } + } + .relation-property-source, + .relation-property-target { + display: inline-block; + max-width: 65px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + .relation-property-source { + padding-right: 6px; + } + .relation-property-target { + padding-left: 6px; + } + } +} diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/react-edge/Relation.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/react-edge/Relation.tsx new file mode 100644 index 000000000..45119b450 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/react-edge/Relation.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import type { NsGraph } from '@antv/xflow'; +import type { RelationCanvasModel } from '../interface'; +import { Popover, Popconfirm, Tooltip } from 'antd'; +import { ScissorOutlined } from '@ant-design/icons'; +import _ from 'lodash'; +import './Relation.less'; + +interface OwnProps { + deleteRelation: Function; +} + +type Props = OwnProps & NsGraph.IReactEdgeProps; + +const Relation = (props: Props) => { + const relation: RelationCanvasModel = props?.data; + + const renderRelationOperationItem = (relation: RelationCanvasModel) => { + const sourcePropertyName = relation?.source; + const targetPropertyName = relation?.target; + return ( +
+
+ + {sourcePropertyName} + + (N:1) + + {targetPropertyName} + +
+ { + props?.deleteRelation(relation.id); + }} + > + + +
+ ); + }; + + const renderPopoverContent = () => { + return ( +
{renderRelationOperationItem(relation)}
+ ); + }; + + return ( + +
{1}
+
+ ); +}; + +export default Relation; diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/react-node/Entity.less b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/react-node/Entity.less new file mode 100644 index 000000000..5a727bab1 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/react-node/Entity.less @@ -0,0 +1,122 @@ +.entity-container { + width: 100%; + height: 100%; + background-color: white; + border-radius: 2px; + + &.fact { + border: 1px solid #cdddfd; + + &:hover { + border: 1px solid #1890ff; + } + } + + &.dim { + border: 1px solid #decfea; + + &:hover { + border: 1px solid #1890ff; + } + } + + &.other { + border: 1px solid #ced4de; + + &:hover { + border: 1px solid #1890ff; + } + } + + .content { + width: calc(100% - 2px); + height: calc(100% - 2px); + margin: 1px 1px; + + &.fact { + background-color: #cdddfd; + } + + &.dim { + background-color: #decfea; + } + + &.other { + background-color: #ced4de; + } + + .head { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + width: calc(100% - 12px); + height: 38px; + margin-left: 6px; + font-size: 12px; + + .type { + padding-right: 8px; + } + + .del-icon { + cursor: pointer; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + size: 16px; + font-size: 14px; + + &:hover { + opacity: 0.6; + color: #1890ff; + } + } + } + + .body { + width: calc(100% - 12px); + height: calc(100% - 36px - 6px); + margin-bottom: 6px; + margin-left: 6px; + overflow: auto; + background-color: white; + cursor: pointer; + + .body-item { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + width: 100%; + height: 28px; + color: #595959; + font-size: 12px; + border-bottom: 1px solid rgba(206, 212, 222, 0.2); + + .name { + margin-left: 6px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + .pk, + .fk { + width: 12px; + margin-right: 6px; + color: #ffd666; + font-family: HelveticaNeue-CondensedBold; + } + } + + .type { + margin-right: 8px; + color: #bfbfbf; + font-size: 8px; + } + } + } + } +} diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/react-node/Entity.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/react-node/Entity.tsx new file mode 100644 index 000000000..8f3bff0cc --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/react-node/Entity.tsx @@ -0,0 +1,61 @@ +import React from 'react' +import type { NsGraph } from '@antv/xflow' +import type { EntityCanvasModel, EntityProperty } from '../interface' +import { BarsOutlined, DeleteOutlined } from '@ant-design/icons' +import { EntityType } from '../const' + +import './Entity.less' + +interface OwnProps { + deleteNode: Function +} + +type Props = OwnProps & NsGraph.IReactNodeProps + +const Entity = (props: Props) => { + const entity: EntityCanvasModel = props?.data + + const getCls = () => { + if (entity?.entityType === EntityType.FACT) { + return 'fact' + } + if (entity?.entityType === EntityType.DIM) { + return 'dim' + } + if (entity?.entityType === EntityType.OTHER) { + return 'other' + } + return '' + } + return ( +
+
+
+
+ + {entity?.entityName} +
+
props.deleteNode(entity?.id)}> + +
+
+
+ {entity?.properties?.map((property: EntityProperty) => { + return ( +
+
+ {property?.isPK && PK} + {property?.isFK && FK} + {property?.propertyName} +
+
{property.propertyType}
+
+ ) + })} +
+
+
+ ) +} + +export default Entity diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/service.ts b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/service.ts new file mode 100644 index 000000000..d41037504 --- /dev/null +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/HeadlessFlows/service.ts @@ -0,0 +1,129 @@ +import type { NsGraph, NsNodeCmd, NsEdgeCmd } from '@antv/xflow' +import { mockEntityData, mockRelationData } from './mock' + +/** mock后端接口调用 */ +export namespace MockApi { + /** 加载画布数据 */ + export const loadGraphData = async () => { + const promise: Promise = new Promise(resolve => { + setTimeout(() => { + /** 链接桩样式配置, 将具有相同行为和外观的链接桩归为同一组 */ + const portAttrs = { + circle: { + r: 7, + stroke: '#31d0c6', + strokeWidth: 2, + fill: '#fff', + /** 链接桩在连线交互时可以被连接上 */ + magnet: true, + }, + } + const nodes: NsGraph.INodeConfig[] = mockEntityData?.map(entity => { + const nodeConfig: NsGraph.INodeConfig = { + ...entity, + renderKey: 'NODE1', + ports: { + groups: { + top: { + position: 'top', + attrs: portAttrs, + }, + right: { + position: 'right', + attrs: portAttrs, + }, + bottom: { + position: 'bottom', + attrs: portAttrs, + }, + left: { + position: 'left', + attrs: portAttrs, + }, + }, + items: [ + { id: 'top_port', group: 'top' }, + { id: 'right_port', group: 'right' }, + { id: 'bottom_port', group: 'bottom' }, + { id: 'left_port', group: 'left' }, + ], + }, + } + return nodeConfig + }) + const edges: NsGraph.IEdgeConfig[] = mockRelationData?.map(relation => { + const edgeConfig: NsGraph.IEdgeConfig = { + ...relation, + renderKey: 'EDGE1', + edgeContentWidth: 20, + edgeContentHeight: 20, + /** 设置连线样式 */ + attrs: { + line: { + stroke: '#d8d8d8', + strokeWidth: 1, + targetMarker: { + name: 'classic', + }, + }, + }, + } + return edgeConfig + }) + const graphData = { nodes, edges } + resolve(graphData) + }, 100) + }) + const res = await promise + return res + } + + /** 添加节点 */ + export const addNode: NsNodeCmd.AddNode.IArgs['createNodeService'] = async args => { + const { nodeConfig } = args + const promise: Promise = new Promise(resolve => { + setTimeout(() => { + resolve({ + ...nodeConfig, + }) + }, 1000) + }) + const res = await promise + return res + } + /** 删除节点 */ + export const delNode: NsNodeCmd.DelNode.IArgs['deleteNodeService'] = async args => { + const { nodeConfig } = args + const promise: Promise = new Promise(resolve => { + setTimeout(() => { + resolve(true) + }, 1000) + }) + const res = await promise + return res + } + /** 添加边 */ + export const addEdge: NsEdgeCmd.AddEdge.IArgs['createEdgeService'] = async args => { + const { edgeConfig } = args + const promise: Promise = new Promise(resolve => { + setTimeout(() => { + resolve({ + ...edgeConfig, + }) + }, 1000) + }) + const res = await promise + return res + } + /** 删除边 */ + export const delEdge: NsEdgeCmd.DelEdge.IArgs['deleteEdgeService'] = async args => { + const { edgeConfig } = args + const promise: Promise = new Promise(resolve => { + setTimeout(() => { + resolve(true) + }, 1000) + }) + const res = await promise + return res + } +} diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Insights/Market.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Insights/Market.tsx index ef10ff3c1..95176013e 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Insights/Market.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Insights/Market.tsx @@ -6,10 +6,10 @@ import type { Dispatch } from 'umi'; import { connect, useModel } from 'umi'; import type { StateType } from '../model'; import { SENSITIVE_LEVEL_ENUM } from '../constant'; -import { getTagList, deleteTag, batchUpdateTagStatus } from '../service'; +import { getTagList, deleteTag, batchUpdateTagStatus, getTagObjectList } from '../service'; import TagFilter from './components/TagFilter'; import TagInfoCreateForm from './components/TagInfoCreateForm'; -import { SemanticNodeType, StatusEnum } from '../enum'; +import { StatusEnum } from '../enum'; import moment from 'moment'; import styles from './style.less'; import { ISemantic } from '../data'; @@ -54,12 +54,27 @@ const ClassMetricTable: React.FC = ({ domainManger, dispatch }) => { const [hasAllPermission, setHasAllPermission] = useState(true); + const [tagObjectList, setTagObjectList] = useState([]); + const actionRef = useRef(); useEffect(() => { - queryTagList(filterParams); + queryTagObjectList(); }, []); + const queryTagObjectList = async () => { + const { code, msg, data } = await getTagObjectList({}); + if (code === 200) { + setTagObjectList(data); + const target = data[0]; + if (target) { + queryTagList({ ...filterParams, tagObjectId: target.id }); + } + return; + } + message.error(msg); + }; + const queryBatchUpdateStatus = async (ids: React.Key[], status: StatusEnum) => { if (Array.isArray(ids) && ids.length === 0) { return; @@ -167,17 +182,26 @@ const ClassMetricTable: React.FC = ({ domainManger, dispatch }) => { width: 300, render: columnsConfig.description.render, }, + // { + // dataIndex: 'status', + // title: '状态', + // width: 180, + // search: false, + // render: columnsConfig.state.render, + // }, { - dataIndex: 'status', - title: '状态', - width: 180, + dataIndex: 'domainName', + title: '所属主题域', + search: false, + }, + { + dataIndex: 'tagObjectName', + title: '所属对象', search: false, - render: columnsConfig.state.render, }, { dataIndex: 'createdBy', title: '创建人', - // width: 150, search: false, }, { @@ -234,7 +258,7 @@ const ClassMetricTable: React.FC = ({ domainManger, dispatch }) => { const handleFilterChange = async (filterParams: { key: string; - sensitiveLevel: string[]; + sensitiveLevel: string; showFilter: string[]; type: string; }) => { @@ -296,6 +320,7 @@ const ClassMetricTable: React.FC = ({ domainManger, dispatch }) => { <>
{ if (_.showType !== undefined) { @@ -321,22 +346,22 @@ const ClassMetricTable: React.FC = ({ domainManger, dispatch }) => { return false; }} sticky={{ offsetHeader: 0 }} - rowSelection={{ - type: 'checkbox', - ...rowSelection, - }} - toolBarRender={() => [ - { - queryBatchUpdateStatus(selectedRowKeys, StatusEnum.DELETED); - }} - hiddenList={['batchDownload']} - disabledList={hasAllPermission ? [] : ['batchStart', 'batchStop', 'batchDelete']} - onMenuClick={onMenuClick} - />, - ]} + // rowSelection={{ + // type: 'checkbox', + // ...rowSelection, + // }} + // toolBarRender={() => [ + // { + // queryBatchUpdateStatus(selectedRowKeys, StatusEnum.DELETED); + // }} + // hiddenList={['batchDownload', 'batchStart', 'batchStop']} + // disabledList={hasAllPermission ? [] : ['batchStart', 'batchDelete']} + // onMenuClick={onMenuClick} + // />, + // ]} loading={loading} onChange={(data: any) => { const { current, pageSize, total } = data; diff --git a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Insights/components/TagFilter.tsx b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Insights/components/TagFilter.tsx index 2fd760623..fd06e8f23 100644 --- a/webapp/packages/supersonic-fe/src/pages/SemanticModel/Insights/components/TagFilter.tsx +++ b/webapp/packages/supersonic-fe/src/pages/SemanticModel/Insights/components/TagFilter.tsx @@ -1,20 +1,22 @@ -import { Form, Input, Space, Row, Col, Switch } from 'antd'; +import { Form, Input, Space, Row, Col, Switch, Select } from 'antd'; import StandardFormRow from '@/components/StandardFormRow'; import TagSelect from '@/components/TagSelect'; import React, { useEffect } from 'react'; import { SENSITIVE_LEVEL_OPTIONS } from '../../constant'; import { SearchOutlined } from '@ant-design/icons'; import DomainTreeSelect from '../../components/DomainTreeSelect'; +import { ISemantic } from '../../data'; import styles from '../style.less'; const FormItem = Form.Item; type Props = { + tagObjectList: ISemantic.ITagObjectItem[]; initFilterValues?: any; onFiltersChange: (_: any, values: any) => void; }; -const TagFilter: React.FC = ({ initFilterValues = {}, onFiltersChange }) => { +const TagFilter: React.FC = ({ tagObjectList, initFilterValues = {}, onFiltersChange }) => { const [form] = Form.useForm(); useEffect(() => { @@ -23,6 +25,14 @@ const TagFilter: React.FC = ({ initFilterValues = {}, onFiltersChange }) }); }, [form]); + useEffect(() => { + const target = tagObjectList?.[0]; + if (!target) { + return; + } + form.setFieldValue('tagObjectId', target.id); + }, [tagObjectList]); + const handleValuesChange = (value: any, values: any) => { localStorage.setItem('metricMarketShowType', !!values.showType ? '1' : '0'); onFiltersChange(value, values); @@ -98,17 +108,20 @@ const TagFilter: React.FC = ({ initFilterValues = {}, onFiltersChange })
- {/* - - + + + + + + + + + + + +