[improvement][headless-fe] Optimized the tag setting system. (#846)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* [improvement][semantic-fe] Added field type and metric type to the metric creation options.

* [improvement][semantic-fe] The organization structure selection feature has been added to the permission management.

* [improvement][semantic-fe] Improved user experience for the metric list.

* [improvement][semantic-fe] fix update the metric list.

* [improvement][headless-fe] Added view management functionality.

* [improvement][headless-fe] The view management functionality has been added. This feature allows users to create, edit, and manage different views within the system.

* [improvement][headless-fe] Added model editing side effect detection.

* [improvement][headless-fe] Fixed the logic error in view editing.

* [improvement][headless-fe] Fixed the issue with initializing dimension associations in metric settings.

* [improvement][headless-fe] Added the ability to hide the Q&A settings entry point.

* [improvement][headless-fe] Fixed the issue with selecting search results in metric field creation.

* [improvement][headless-fe] Added search functionality to the field list in model editing.

* [improvement][headless-fe] fix the field list in model editing

* [improvement][headless-fe] Restructured the data for the dimension value settings interface.

* [improvement][headless-fe] Added dynamic variable functionality to model creation based on SQL scripts.

* [improvement][headless-fe] Added support for passing dynamic variables as parameters in the executeSql function.

* [improvement][headless-fe] Resolved the issue where users were unable to select all options for dimensions, metrics, and fields in the metric generation process.

* [improvement][headless-fe] Replaced the term "view" with "dataset"

* [improvement][headless-fe] Added the ability to export metrics and dimensions to a specific target.

* [improvement][headless-fe] Enhanced dataset creation to support the tag mode.

* [improvement][headless-fe] Added tag value setting.

* [improvement][headless-fe] Optimized the tag setting system.
This commit is contained in:
tristanliu
2024-03-21 00:53:16 +08:00
committed by GitHub
parent 754902a67e
commit 01bfb57149
45 changed files with 2189 additions and 128 deletions

View File

@@ -0,0 +1 @@
.create-entity-container {}

View File

@@ -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<boolean>(false)
const [currentEntityType, setCurrentEntityType] = React.useState<EntityType>(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 (
<Modal
title="创建模型"
visible={visible}
confirmLoading={confirmLoading}
wrapClassName="create-entity-container"
okText="确定"
cancelText="取消"
onOk={hanldeOk}
onCancel={() => onCancel()}
mask={false}
centered
destroyOnClose={true}
>
<Form form={form}>
<Form.Item
{...formItemLayout}
name="entityType"
label="模型类型"
rules={[{ required: true }]}
initialValue={currentEntityType}
>
<Radio.Group onChange={onChange}>
{_.map(EntityType, (type: EntityType) => {
return (
<Radio value={type} key={type}>
{EntityTypeDisplay[type]}
</Radio>
);
})}
</Radio.Group>
</Form.Item>
<Form.Item
{...formItemLayout}
name="displayName"
label="中文名"
rules={
[
{
required: true,
validator: (rule, v, callback) => {
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={'用户创建的表'}
>
<Input placeholder="请输入中文名称" autoComplete="off" />
</Form.Item>
<Form.Item
{...formItemLayout}
name="name"
label="英文名"
rules={
[
{
required: true,
validator: (rule, v, callback) => {
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'}
>
<Input placeholder="请输入英文名" autoComplete="off" />
</Form.Item>
</Form>
</Modal>
);
}
export default CreateEntityModal

View File

@@ -0,0 +1 @@
.create-relation-container {}

View File

@@ -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<boolean>(false)
const [form] = Form.useForm()
const handleOK = () => {
form.validateFields().then(values => {
setConfirmLoading(true)
const cb = () => {
setConfirmLoading(false)
}
onOk({ ...values, cb })
})
}
return (
<Modal
title="关联模型"
visible={visible}
confirmLoading={confirmLoading}
wrapClassName="create-relation-container"
okText="确定"
cancelText="取消"
onOk={handleOK}
onCancel={onCancel}
mask={false}
centered
destroyOnClose={true}
>
<Form form={form}>
<Form.Item
{...formItemLayout}
name="SOURCE_GUID"
label="SOURCE_GUID"
rules={[{ required: true }]}
initialValue={`${sourceEntity?.entityName || ''}(${sourceEntity?.entityId || ''})`}
>
<Input />
</Form.Item>
<Form.Item
{...formItemLayout}
name="TARGET_GUID"
label="TARGET_GUID"
rules={[{ required: true }]}
initialValue={`${targetEntity?.entityName || ''}(${targetEntity?.entityId || ''})`}
>
<Input />
</Form.Item>
<Form.Item
{...formItemLayout}
name="RELATION_TYPE"
label="选择关联关系"
rules={[{ required: true }]}
initialValue={'N:1'}
>
<Select placeholder="请选择关联关系">
<Select.Option value="N:1"></Select.Option>
<Select.Option value="1:N"></Select.Option>
</Select>
</Form.Item>
</Form>
</Modal>
)
}
export default CreateRelationModal

View File

@@ -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;
}
}

View File

@@ -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 (
<div className="xflow-er-solution-toolbar">
<div className="icon" onClick={() => onAddNodeClick()}>
<span></span>
<PlusCircleOutlined />
</div>
<div className="icon" onClick={() => onConnectEdgeClick()}>
<span></span>
<LinkOutlined />
</div>
<div
className={`icon ${selectedNodes?.length > 0 ? '' : 'disabled'}`}
onClick={() => onDeleteNodeClick()}
>
<span></span>
<DeleteOutlined />
</div>
</div>
)
}
export default GraphToolbar

View File

@@ -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
})
})

View File

@@ -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,
},
})
})

View File

@@ -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('删除连线成功!')
}
}

View File

@@ -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]: '其他表',
}

View File

@@ -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;
}
}

View File

@@ -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<Props> = (demoProps: Props) => {
/** XFlow应用实例 */
const app = useXFlowApp();
/** 画布配置项 */
const graphConfig = useGraphConfig();
/** 预设XFlow画布需要渲染的React节点 / 边 */
graphConfig.setNodeRender('NODE1', (props) => {
return <Entity {...props} deleteNode={handleDeleteNode} />;
});
graphConfig.setEdgeRender('EDGE1', (props) => {
return <Relation {...props} deleteRelation={handleDeleteEdge} />;
});
/** 命令配置项 */
const cmdConfig = useCmdConfig();
/** 快捷键配置项 */
const keybindingConfig = useKeybindingConfig();
/** 是否画布处于可以新建节点状态 */
const [graphStatuts, setGraphStatus] = useState<string>('NORMAL');
/** 是否展示新建节点弹窗 */
const [isShowCreateNodeModal, setIsShowCreateNodeModal] = useState<boolean>(false);
/** 是否展示新建关联关系弹窗 */
const [isShowCreateRelationModal, setIsShowCreateRelationModal] = useState<boolean>(false);
/** 连线source数据 */
const [relationSourceData, setRelationSourceData] = useState<EntityCanvasModel>(undefined);
/** 连线target数据 */
const [relationTargetData, setRelationTargetData] = useState<EntityCanvasModel>(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<SVGElement>;
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 (
<XFlowAppProvider>
<div
onMouseMove={(e) => {
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',
});
}
}}
>
<XFlow className="xflow-er-solution-container" commandConfig={cmdConfig} onLoad={onLoad}>
<GraphToolbar
onAddNodeClick={() => {
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);
}}
/>
<XFlowCanvas config={graphConfig}>
<CanvasMiniMap nodeFillColor="#ced4de" minimapOptions={{}} />
<CanvasScaleToolbar position={{ top: 12, left: 20 }} />
<CanvasSnapline />
</XFlowCanvas>
<KeyBindings config={keybindingConfig} />
{/** 占位空节点 */}
{graphStatuts === 'CREATE' && (
<div
className="cursor-tip-container"
ref={(ref) => {
ref && (cursorTipRef = ref);
}}
>
<div className="draft-entity-container">
<div></div>
</div>
</div>
)}
{/** 创建节点弹窗 */}
<CreateNodeModal
visible={isShowCreateNodeModal}
onOk={handleCreateNode}
onCancel={() => {
setIsShowCreateNodeModal(false);
}}
/>
{/** 创建关联关系弹窗 */}
<CreateRelationModal
visible={isShowCreateRelationModal}
sourceEntity={relationSourceData}
targetEntity={relationTargetData}
onOk={handleCreateEdge}
onCancel={() => {
setIsShowCreateRelationModal(false);
}}
/>
</XFlow>
</div>
</XFlowAppProvider>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(DomainManger);

View File

@@ -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

View File

@@ -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',
},
];

View File

@@ -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;
}
}
}

View File

@@ -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 (
<div className="relation-operation-item" key={relation.id}>
<div className="relation-operation-item-content">
<Tooltip placement="top" title={sourcePropertyName}>
<span className="relation-property-source">{sourcePropertyName}</span>
</Tooltip>
(N:1)
<Tooltip placement="top" title={targetPropertyName}>
<span className="relation-property-target">{targetPropertyName}</span>
</Tooltip>
</div>
<Popconfirm
placement="leftTop"
title="你确定要删除该关系吗"
okText="确定"
cancelText="取消"
onConfirm={() => {
props?.deleteRelation(relation.id);
}}
>
<ScissorOutlined />
</Popconfirm>
</div>
);
};
const renderPopoverContent = () => {
return (
<div className="relation-operation-container">{renderRelationOperationItem(relation)}</div>
);
};
return (
<Popover
trigger={'hover'}
content={renderPopoverContent()}
overlayClassName="relation-operation-popover"
>
<div className="relation-count-container">{1}</div>
</Popover>
);
};
export default Relation;

View File

@@ -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;
}
}
}
}
}

View File

@@ -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 (
<div className={`entity-container ${getCls()}`}>
<div className={`content ${getCls()}`}>
<div className="head">
<div>
<BarsOutlined className="type" />
<span>{entity?.entityName}</span>
</div>
<div className="del-icon" onClick={() => props.deleteNode(entity?.id)}>
<DeleteOutlined />
</div>
</div>
<div className="body">
{entity?.properties?.map((property: EntityProperty) => {
return (
<div className="body-item" key={property.propertyId}>
<div className="name">
{property?.isPK && <span className="pk">PK</span>}
{property?.isFK && <span className="fk">FK</span>}
{property?.propertyName}
</div>
<div className="type">{property.propertyType}</div>
</div>
)
})}
</div>
</div>
</div>
)
}
export default Entity

View File

@@ -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<NsGraph.IGraphData> = 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<NsGraph.INodeConfig> = 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<boolean> = 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<NsGraph.IEdgeConfig> = 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<boolean> = new Promise(resolve => {
setTimeout(() => {
resolve(true)
}, 1000)
})
const res = await promise
return res
}
}