mirror of
https://github.com/tencentmusic/supersonic.git
synced 2026-01-23 12:01:18 +08:00
[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:
@@ -0,0 +1 @@
|
||||
.create-entity-container {}
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
.create-relation-container {}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -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('删除连线成功!')
|
||||
}
|
||||
}
|
||||
@@ -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]: '其他表',
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user