first commit

This commit is contained in:
jerryjzhang
2023-06-12 18:44:01 +08:00
commit dc4fc69b57
879 changed files with 573090 additions and 0 deletions

View File

@@ -0,0 +1,209 @@
import type { HookHub, ICmdHooks as IHooks, NsGraph } from '@antv/xflow';
import { Deferred, ManaSyringe } from '@antv/xflow';
import { Modal, ConfigProvider } from 'antd';
import type { IArgsBase, ICommandHandler } from '@antv/xflow';
import { ICommandContextProvider } from '@antv/xflow';
import { DATASOURCE_NODE_RENDER_ID } from '../constant';
import { CustomCommands } from './constants';
import 'antd/es/modal/style/index.css';
export namespace NsConfirmModalCmd {
/** Command: 用于注册named factory */
// eslint-disable-next-line
export const command = CustomCommands.SHOW_CONFIRM_MODAL;
/** hook name */
// eslint-disable-next-line
export const hookKey = 'confirmModal';
/** hook 参数类型 */
export interface IArgs extends IArgsBase {
nodeConfig: NsGraph.INodeConfig;
confirmModalCallBack: IConfirmModalService;
}
export interface IConfirmModalService {
(): Promise<any>;
}
/** hook handler 返回类型 */
export type IResult = any;
/** hooks 类型 */
export interface ICmdHooks extends IHooks {
confirmModal: HookHub<IArgs, IResult>;
}
}
const deleteDataSourceConfirmNode = (name: string) => {
return (
<>
<span style={{ color: '#296DF3', fontWeight: 'bold' }}>{name}</span>
</>
);
};
// prettier-ignore
type ICommand = ICommandHandler<NsConfirmModalCmd.IArgs, NsConfirmModalCmd.IResult, NsConfirmModalCmd.ICmdHooks>;
@ManaSyringe.injectable()
/** 部署画布数据 */
export class ConfirmModalCommand implements ICommand {
/** api */
@ManaSyringe.inject(ICommandContextProvider) contextProvider!: ICommand['contextProvider'];
/** 执行Cmd */
execute = async () => {
const ctx = this.contextProvider();
const { args } = ctx.getArgs();
const hooks = ctx.getHooks();
await hooks.confirmModal.call(args, async (confirmArgs: NsConfirmModalCmd.IArgs) => {
const { nodeConfig, confirmModalCallBack } = confirmArgs;
const { renderKey, label } = nodeConfig;
if (!nodeConfig.modalProps?.modalContent) {
let modalContent = <></>;
if (renderKey === DATASOURCE_NODE_RENDER_ID) {
modalContent = deleteDataSourceConfirmNode(label!);
}
nodeConfig.modalProps = {
...(nodeConfig.modalProps || {}),
modalContent,
};
}
const getAppContext: IGetAppCtx = () => {
return {
confirmModalCallBack,
};
};
const x6Graph = await ctx.getX6Graph();
const cell = x6Graph.getCellById(nodeConfig.id);
if (!cell || !cell.isNode()) {
throw new Error(`${nodeConfig.id} is not a valid node`);
}
/** 通过modal 获取 new name */
await showModal(nodeConfig, getAppContext);
return;
});
// ctx.setResult(result);
return this;
};
/** undo cmd */
undo = async () => {
if (this.isUndoable()) {
const ctx = this.contextProvider();
ctx.undo();
}
return this;
};
/** redo cmd */
redo = async () => {
if (!this.isUndoable()) {
await this.execute();
}
return this;
};
isUndoable(): boolean {
const ctx = this.contextProvider();
return ctx.isUndoable();
}
}
export interface IGetAppCtx {
(): {
confirmModalCallBack: NsConfirmModalCmd.IConfirmModalService;
};
}
export type IModalInstance = ReturnType<typeof Modal.confirm>;
function showModal(node: NsGraph.INodeConfig, getAppContext: IGetAppCtx) {
/** showModal 返回一个Promise */
const defer = new Deferred<string | void>();
const modalTitle = node.modalProps?.title;
const modalContent = node.modalProps?.modalContent;
/** modal确认保存逻辑 */
class ModalCache {
static modal: IModalInstance;
}
/** modal确认保存逻辑 */
const onOk = async () => {
const { modal } = ModalCache;
const appContext = getAppContext();
const { confirmModalCallBack } = appContext;
try {
modal.update({ okButtonProps: { loading: true } });
/** 执行 confirm回调*/
if (confirmModalCallBack) {
await confirmModalCallBack();
}
/** 更新成功后关闭modal */
onHide();
} catch (error) {
console.error(error);
/** 如果resolve空字符串则不更新 */
modal.update({ okButtonProps: { loading: false } });
}
};
/** modal销毁逻辑 */
const onHide = () => {
modal.destroy();
ModalCache.modal = null as any;
container.destroy();
};
/** modal内容 */
const ModalContent = () => {
return (
<div>
<ConfigProvider>{modalContent}</ConfigProvider>
</div>
);
};
/** 创建modal dom容器 */
const container = createContainer();
/** 创建modal */
const modal = Modal.confirm({
title: modalTitle,
content: <ModalContent />,
getContainer: () => {
return container.element;
},
okButtonProps: {
onClick: (e) => {
e.stopPropagation();
onOk();
},
},
onCancel: () => {
onHide();
},
afterClose: () => {
onHide();
},
});
/** 缓存modal实例 */
ModalCache.modal = modal;
/** showModal 返回一个Promise用于await */
return defer.promise;
}
const createContainer = () => {
const div = document.createElement('div');
div.classList.add('xflow-modal-container');
window.document.body.append(div);
return {
element: div,
destroy: () => {
window.document.body.removeChild(div);
},
};
};

View File

@@ -0,0 +1,89 @@
import type {
NsGraphCmd,
ICmdHooks as IHooks,
NsGraph,
IArgsBase,
ICommandHandler,
HookHub,
} from '@antv/xflow';
import { XFlowGraphCommands, ManaSyringe } from '@antv/xflow';
import { ICommandContextProvider } from '@antv/xflow';
import { CustomCommands } from './constants';
// prettier-ignore
type ICommand = ICommandHandler<NsDeployDagCmd.IArgs, NsDeployDagCmd.IResult, NsDeployDagCmd.ICmdHooks>;
export namespace NsDeployDagCmd {
/** Command: 用于注册named factory */
// eslint-disable-next-line
export const command = CustomCommands.DEPLOY_SERVICE;
/** hook name */
// eslint-disable-next-line
export const hookKey = 'deployDag';
/** hook 参数类型 */
export interface IArgs extends IArgsBase {
deployDagService: IDeployDagService;
}
export interface IDeployDagService {
(meta: NsGraph.IGraphMeta, data: NsGraph.IGraphData): Promise<{ success: boolean }>;
}
/** hook handler 返回类型 */
export interface IResult {
success: boolean;
}
/** hooks 类型 */
export interface ICmdHooks extends IHooks {
deployDag: HookHub<IArgs, IResult>;
}
}
@ManaSyringe.injectable()
/** 部署画布数据 */
export class DeployDagCommand implements ICommand {
/** api */
@ManaSyringe.inject(ICommandContextProvider) contextProvider!: ICommand['contextProvider'];
/** 执行Cmd */
execute = async () => {
const ctx = this.contextProvider();
const { args } = ctx.getArgs();
const hooks = ctx.getHooks();
const result = await hooks.deployDag.call(args, async (handlerArgs) => {
const { commandService, deployDagService } = handlerArgs;
/** 执行Command */
await commandService!.executeCommand<NsGraphCmd.SaveGraphData.IArgs>(
XFlowGraphCommands.SAVE_GRAPH_DATA.id,
{
saveGraphDataService: async (meta, graph) => {
await deployDagService(meta, graph);
},
},
);
return { success: true };
});
ctx.setResult(result);
return this;
};
/** undo cmd */
undo = async () => {
if (this.isUndoable()) {
const ctx = this.contextProvider();
ctx.undo();
}
return this;
};
/** redo cmd */
redo = async () => {
if (!this.isUndoable()) {
await this.execute();
}
return this;
};
isUndoable(): boolean {
const ctx = this.contextProvider();
return ctx.isUndoable();
}
}

View File

@@ -0,0 +1,255 @@
import type { HookHub, ICmdHooks as IHooks, NsGraph, IModelService } from '@antv/xflow';
import { Deferred, ManaSyringe } from '@antv/xflow';
import type { FormInstance } from 'antd';
import { Modal, Form, Input, ConfigProvider } from 'antd';
import type { IArgsBase, ICommandHandler, IGraphCommandService } from '@antv/xflow';
import { ICommandContextProvider } from '@antv/xflow';
import { CustomCommands } from './constants';
import 'antd/es/modal/style/index.css';
// prettier-ignore
type ICommand = ICommandHandler<NsRenameNodeCmd.IArgs, NsRenameNodeCmd.IResult, NsRenameNodeCmd.ICmdHooks>;
export namespace NsRenameNodeCmd {
/** Command: 用于注册named factory */
// eslint-disable-next-line
export const command = CustomCommands.SHOW_RENAME_MODAL;
/** hook name */
// eslint-disable-next-line
export const hookKey = 'renameNode';
/** hook 参数类型 */
export interface IArgs extends IArgsBase {
nodeConfig: NsGraph.INodeConfig;
updateNodeNameService: IUpdateNodeNameService;
}
export interface IUpdateNodeNameService {
(newName: string, nodeConfig: NsGraph.INodeConfig, meta: NsGraph.IGraphMeta): Promise<{
err: string | null;
nodeName: string;
}>;
}
/** hook handler 返回类型 */
export interface IResult {
err: string | null;
preNodeName?: string;
currentNodeName?: string;
}
/** hooks 类型 */
export interface ICmdHooks extends IHooks {
renameNode: HookHub<IArgs, IResult>;
}
}
@ManaSyringe.injectable()
/** 部署画布数据 */
// prettier-ignore
export class RenameNodeCommand implements ICommand {
/** api */
@ManaSyringe.inject(ICommandContextProvider) contextProvider!: ICommand['contextProvider'];
/** 执行Cmd */
execute = async () => {
const ctx = this.contextProvider();
// const app = useXFlowApp();
const { args } = ctx.getArgs();
const hooks = ctx.getHooks();
const result = await hooks.renameNode.call(args, async (args) => {
const { nodeConfig, graphMeta, commandService, modelService, updateNodeNameService } = args;
const preNodeName = nodeConfig.label;
const getAppContext: IGetAppCtx = () => {
return {
graphMeta,
commandService,
modelService,
updateNodeNameService,
};
};
const x6Graph = await ctx.getX6Graph();
const cell = x6Graph.getCellById(nodeConfig.id);
const nodes = x6Graph.getNodes();
const edges = x6Graph.getEdges();
nodes.forEach((node) => {
if (node !== cell) {
x6Graph.removeCell(node);
}
});
edges.forEach((edge) => {
x6Graph.removeEdge(edge);
});
if (!cell || !cell.isNode()) {
throw new Error(`${nodeConfig.id} is not a valid node`);
}
/** 通过modal 获取 new name */
const newName = await showModal(nodeConfig, getAppContext);
/** 更新 node name */
if (newName) {
const cellData = cell.getData<NsGraph.INodeConfig>();
cell.setData({ ...cellData, label: newName } as NsGraph.INodeConfig);
return { err: null, preNodeName, currentNodeName: newName };
}
return { err: null, preNodeName, currentNodeName: '' };
});
ctx.setResult(result);
return this;
};
/** undo cmd */
undo = async () => {
if (this.isUndoable()) {
const ctx = this.contextProvider();
ctx.undo();
}
return this;
};
/** redo cmd */
redo = async () => {
if (!this.isUndoable()) {
await this.execute();
}
return this;
};
isUndoable(): boolean {
const ctx = this.contextProvider();
return ctx.isUndoable();
}
}
export interface IGetAppCtx {
(): {
graphMeta: NsGraph.IGraphMeta;
commandService: IGraphCommandService;
modelService: IModelService;
updateNodeNameService: NsRenameNodeCmd.IUpdateNodeNameService;
};
}
export type IModalInstance = ReturnType<typeof Modal.confirm>;
export interface IFormProps {
newNodeName: string;
}
const layout = {
labelCol: { span: 5 },
wrapperCol: { span: 19 },
};
function showModal(node: NsGraph.INodeConfig, getAppContext: IGetAppCtx) {
/** showModal 返回一个Promise */
const defer = new Deferred<string | void>();
/** modal确认保存逻辑 */
class ModalCache {
static modal: IModalInstance;
static form: FormInstance<IFormProps>;
}
/** modal确认保存逻辑 */
const onOk = async () => {
const { form, modal } = ModalCache;
const appContext = getAppContext();
const { updateNodeNameService, graphMeta } = appContext;
try {
modal.update({ okButtonProps: { loading: true } });
await form.validateFields();
const values = await form.getFieldsValue();
const newName: string = values.newNodeName;
/** 执行 backend service */
if (updateNodeNameService) {
const { err, nodeName } = await updateNodeNameService(newName, node, graphMeta);
if (err) {
throw new Error(err);
}
defer.resolve(nodeName);
}
/** 更新成功后关闭modal */
onHide();
} catch (error) {
console.error(error);
/** 如果resolve空字符串则不更新 */
modal.update({ okButtonProps: { loading: false } });
}
};
/** modal销毁逻辑 */
const onHide = () => {
modal.destroy();
ModalCache.form = null as any;
ModalCache.modal = null as any;
container.destroy();
};
/** modal内容 */
const ModalContent = () => {
const [form] = Form.useForm<IFormProps>();
/** 缓存form实例 */
ModalCache.form = form;
return (
<div>
<ConfigProvider>
<Form form={form} {...layout} initialValues={{ newNodeName: node.label }}>
<Form.Item
name="newNodeName"
label="节点名"
rules={[
{ required: true, message: '请输入新节点名' },
{ min: 3, message: '节点名不能少于3个字符' },
]}
>
<Input />
</Form.Item>
</Form>
</ConfigProvider>
</div>
);
};
/** 创建modal dom容器 */
const container = createContainer();
/** 创建modal */
const modal = Modal.confirm({
title: '重命名',
content: <ModalContent />,
getContainer: () => {
return container.element;
},
okButtonProps: {
onClick: (e) => {
e.stopPropagation();
onOk();
},
},
onCancel: () => {
onHide();
},
afterClose: () => {
onHide();
},
});
/** 缓存modal实例 */
ModalCache.modal = modal;
/** showModal 返回一个Promise用于await */
return defer.promise;
}
const createContainer = () => {
const div = document.createElement('div');
div.classList.add('xflow-modal-container');
window.document.body.append(div);
return {
element: div,
destroy: () => {
window.document.body.removeChild(div);
},
};
};

View File

@@ -0,0 +1,88 @@
import type {
ICmdHooks as IHooks,
NsGraph,
IArgsBase,
ICommandHandler,
HookHub,
} from '@antv/xflow';
import { ManaSyringe } from '@antv/xflow';
import { ICommandContextProvider } from '@antv/xflow';
import { CustomCommands } from './constants';
import { getDatasourceRelaList } from '../../service';
// prettier-ignore
type ICommand = ICommandHandler<NsDataSourceRelationCmd.IArgs, NsDataSourceRelationCmd.IResult, NsDataSourceRelationCmd.ICmdHooks>;
export namespace NsDataSourceRelationCmd {
/** Command: 用于注册named factory */
// eslint-disable-next-line
export const command = CustomCommands.DATASOURCE_RELATION;
/** hook name */
// eslint-disable-next-line
export const hookKey = 'dataSourceRelation';
/** hook 参数类型 */
export interface IArgs extends IArgsBase {
dataSourceRelationService: IDataSourceRelationService;
}
export interface IDataSourceRelationService {
(meta: NsGraph.IGraphMeta, data: NsGraph.IGraphData): Promise<{ success: boolean }>;
}
/** hook handler 返回类型 */
export type IResult = any[] | undefined;
/** hooks 类型 */
export interface ICmdHooks extends IHooks {
dataSourceRelation: HookHub<IArgs, IResult>;
}
}
@ManaSyringe.injectable()
/** 部署画布数据 */
export class DataSourceRelationCommand implements ICommand {
/** api */
@ManaSyringe.inject(ICommandContextProvider) contextProvider!: ICommand['contextProvider'];
/** 执行Cmd */
execute = async () => {
const ctx = this.contextProvider();
const { args } = ctx.getArgs();
const hooks = ctx.getHooks();
const graphMeta = await ctx.getGraphMeta();
const domainId = graphMeta?.meta?.domainManger?.selectDomainId;
if (!domainId) {
return this;
}
const result = await hooks.dataSourceRelation.call(args, async () => {
const { code, data } = await getDatasourceRelaList(domainId);
if (code === 200) {
return data;
}
return [];
});
ctx.setResult(result);
ctx.setGlobal('dataSourceRelationList', result);
return this;
};
/** undo cmd */
undo = async () => {
if (this.isUndoable()) {
const ctx = this.contextProvider();
ctx.undo();
}
return this;
};
/** redo cmd */
redo = async () => {
if (!this.isUndoable()) {
await this.execute();
}
return this;
};
isUndoable(): boolean {
const ctx = this.contextProvider();
return ctx.isUndoable();
}
}

View File

@@ -0,0 +1,43 @@
import type { IGraphCommand } from '@antv/xflow';
/** 节点命令 */
export namespace CustomCommands {
const category = '节点操作';
/** 异步请求demo */
export const TEST_ASYNC_CMD: IGraphCommand = {
id: 'xflow:async-cmd',
label: '异步请求',
category,
};
/** 重命名节点弹窗 */
export const SHOW_RENAME_MODAL: IGraphCommand = {
id: 'xflow:rename-node-modal',
label: '打开重命名弹窗',
category,
};
/** 二次确认弹窗 */
export const SHOW_CONFIRM_MODAL: IGraphCommand = {
id: 'xflow:confirm-modal',
label: '打开二次确认弹窗',
category,
};
/** 部署服务 */
export const DEPLOY_SERVICE: IGraphCommand = {
id: 'xflow:deploy-service',
label: '部署服务',
category,
};
export const DATASOURCE_RELATION: IGraphCommand = {
id: 'xflow:datasource-relation',
label: '获取数据源关系数据',
category,
};
/** 查看维度 */
export const VIEW_DIMENSION: IGraphCommand = {
id: 'xflow:view-dimension',
label: '查看维度',
category,
};
}

View File

@@ -0,0 +1,28 @@
import { DeployDagCommand, NsDeployDagCmd } from './CmdDeploy';
import { RenameNodeCommand, NsRenameNodeCmd } from './CmdRenameNodeModal';
import { ConfirmModalCommand, NsConfirmModalCmd } from './CmdConfirmModal';
import {
DataSourceRelationCommand,
NsDataSourceRelationCmd,
} from './CmdUpdateDataSourceRelationList';
import type { ICommandContributionConfig } from '@antv/xflow';
/** 注册成为可以执行的命令 */
export const COMMAND_CONTRIBUTIONS: ICommandContributionConfig[] = [
{
...NsDeployDagCmd,
CommandHandler: DeployDagCommand,
},
{
...NsRenameNodeCmd,
CommandHandler: RenameNodeCommand,
},
{
...NsConfirmModalCmd,
CommandHandler: ConfirmModalCommand,
},
{
...NsDataSourceRelationCmd,
CommandHandler: DataSourceRelationCommand,
},
];

View File

@@ -0,0 +1,266 @@
import type { NsGraphCmd } from '@antv/xflow';
import { createCmdConfig, DisposableCollection, XFlowGraphCommands } from '@antv/xflow';
import type { IApplication } from '@antv/xflow';
import type { IGraphPipelineCommand, IGraphCommandService, NsGraph } from '@antv/xflow';
import { GraphApi } from './service';
import { addDataSourceInfoAsDimensionParents } from './utils';
import { COMMAND_CONTRIBUTIONS } from './CmdExtensions';
import { CustomCommands } from './CmdExtensions/constants';
export const useCmdConfig = createCmdConfig((config) => {
// 注册全局Command扩展
config.setCommandContributions(() => COMMAND_CONTRIBUTIONS);
// 设置hook
config.setRegisterHookFn((hooks) => {
const list = [
hooks.graphMeta.registerHook({
name: 'get graph meta from backend',
handler: async (args) => {
args.graphMetaService = GraphApi.queryGraphMeta;
},
}),
hooks.saveGraphData.registerHook({
name: 'save graph data',
handler: async (args) => {
if (!args.saveGraphDataService) {
args.saveGraphDataService = GraphApi.saveGraphData;
}
},
}),
hooks.addNode.registerHook({
name: 'get node config from backend api',
handler: async (args) => {
args.createNodeService = GraphApi.addNode;
},
}),
hooks.delNode.registerHook({
name: 'get edge config from backend api',
handler: async (args) => {
args.deleteNodeService = GraphApi.delNode;
},
}),
hooks.addEdge.registerHook({
name: '获取起始和结束节点的业务数据,并写入在边上',
handler: async (handlerArgs, handler: any) => {
const { commandService } = handlerArgs;
const main = async (args: any) => {
const res = await handler(args);
if (res && res.edgeCell) {
const sourceNode = res.edgeCell.getSourceNode();
const targetNode = res.edgeCell.getTargetNode();
const sourceNodeData = sourceNode?.getData() || {};
const targetNodeData = targetNode?.getData() || {};
res.edgeCell.setData({
sourceNodeData,
targetNodeData,
source: sourceNodeData.id,
target: targetNodeData.id,
});
// 对边进行tooltips设置
res.edgeCell.addTools([
{
name: 'tooltip',
args: {
tooltip: '左键点击进行关系编辑,右键点击进行删除操作',
},
},
]);
if (commandService) {
const initGraphCmdsState: any = commandService.getGlobal('initGraphCmdsSuccess');
if (initGraphCmdsState) {
// 保存图数据
commandService!.executeCommand<NsGraphCmd.SaveGraphData.IArgs>(
XFlowGraphCommands.SAVE_GRAPH_DATA.id,
{
saveGraphDataService: (meta, graphData) =>
GraphApi.saveGraphData!(meta, graphData),
},
);
}
}
return res;
}
};
return main;
},
}),
hooks.delEdge.registerHook({
name: '边删除,并向后台请求删除数据源间关联关系',
handler: async (args) => {
args.deleteEdgeService = GraphApi.delEdge;
},
}),
];
const toDispose = new DisposableCollection();
toDispose.pushAll(list);
return toDispose;
});
});
/** 查询图的节点和边的数据 */
export const initGraphCmds = async (app: IApplication) => {
const { commandService } = app;
await app.executeCommandPipeline([
/** 1. 从服务端获取数据 */
{
commandId: XFlowGraphCommands.LOAD_DATA.id,
getCommandOption: async () => {
commandService.setGlobal('initGraphCmdsSuccess', false);
return {
args: {
loadDataService: GraphApi.loadDataSourceData,
},
};
},
} as IGraphPipelineCommand<NsGraphCmd.GraphLoadData.IArgs>,
/** 2. 执行布局算法 */
{
commandId: XFlowGraphCommands.GRAPH_LAYOUT.id,
getCommandOption: async (ctx) => {
const { graphData } = ctx.getResult();
return {
args: {
layoutType: 'dagre',
layoutOptions: {
type: 'dagre',
/** 布局方向 */
rankdir: 'LR',
/** 节点间距 */
nodesep: 30,
/** 层间距 */
ranksep: 80,
begin: [0, 0],
},
graphData,
},
};
},
} as IGraphPipelineCommand<NsGraphCmd.GraphLayout.IArgs>,
/** 3. 画布内容渲染 */
{
commandId: XFlowGraphCommands.GRAPH_RENDER.id,
getCommandOption: async (ctx) => {
const { graphData } = ctx.getResult();
const { edges, nodes } = graphData;
const filterClassNodeEdges = edges.filter((item: NsGraph.IEdgeConfig) => {
return !item.source.includes('classNodeId-');
});
const filterClassNodeNodes = nodes.filter((item: NsGraph.INodeConfig) => {
return !item.id.includes('classNodeId-');
});
return {
args: {
graphData: {
edges: filterClassNodeEdges,
nodes: filterClassNodeNodes,
},
},
};
},
} as IGraphPipelineCommand<NsGraphCmd.GraphRender.IArgs>,
/** 4. 缩放画布 */
{
commandId: XFlowGraphCommands.GRAPH_ZOOM.id,
getCommandOption: async () => {
commandService.setGlobal('initGraphCmdsSuccess', true);
return {
args: { factor: 'fit', zoomOptions: { maxScale: 0.9 } },
};
},
} as IGraphPipelineCommand<NsGraphCmd.GraphZoom.IArgs>,
// commandService.executeCommand(CustomCommands.DATASOURCE_RELATION.id, {});
{
commandId: CustomCommands.DATASOURCE_RELATION.id,
getCommandOption: async () => {
return {
args: {},
};
},
},
]);
// const nodes = await app.getAllNodes();
// const classNodes = nodes.filter((item) => {
// return item.id.includes('classNodeId');
// });
// if (classNodes?.[0]) {
// const targetClassId = classNodes[0].id;
// await app.commandService.executeCommand<NsNodeCmd.DelNode.IArgs>(
// XFlowNodeCommands.DEL_NODE.id,
// {
// nodeConfig: { id: targetClassId, type: 'class' },
// },
// );
// }
};
/** 查询当前数据源下的维度节点和边的数据 */
export const initDimensionGraphCmds = async (args: {
commandService: IGraphCommandService;
target: NsGraph.INodeConfig;
}) => {
const { commandService, target } = args;
await commandService.executeCommandPipeline([
{
commandId: XFlowGraphCommands.LOAD_DATA.id,
getCommandOption: async () => {
return {
args: {
loadDataService: GraphApi.loadDimensionData,
},
};
},
} as IGraphPipelineCommand<NsGraphCmd.GraphLoadData.IArgs>,
/** 2. 执行布局算法 */
{
commandId: XFlowGraphCommands.GRAPH_LAYOUT.id,
getCommandOption: async (ctx) => {
const { graphData } = ctx.getResult();
const targetData = {
...target.data,
};
delete targetData.x;
delete targetData.y;
const addGraphData = addDataSourceInfoAsDimensionParents(graphData, targetData);
ctx.setResult(addGraphData);
return {
args: {
layoutType: 'dagre',
layoutOptions: {
type: 'dagre',
/** 布局方向 */
rankdir: 'LR',
/** 节点间距 */
nodesep: 30,
/** 层间距 */
ranksep: 80,
begin: [0, 0],
},
graphData: addGraphData,
},
};
},
} as IGraphPipelineCommand<NsGraphCmd.GraphLayout.IArgs>,
/** 3. 画布内容渲染 */
{
commandId: XFlowGraphCommands.GRAPH_RENDER.id,
getCommandOption: async (ctx) => {
const { graphData } = ctx.getResult();
return {
args: {
graphData,
},
};
},
} as IGraphPipelineCommand<NsGraphCmd.GraphRender.IArgs>,
/** 4. 缩放画布 */
{
commandId: XFlowGraphCommands.GRAPH_ZOOM.id,
getCommandOption: async () => {
return {
args: { factor: 'fit', zoomOptions: { maxScale: 0.9 } },
};
},
} as IGraphPipelineCommand<NsGraphCmd.GraphZoom.IArgs>,
]);
};

View File

@@ -0,0 +1,51 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { uuidv4 } from '@antv/xflow';
import { XFlowNodeCommands } from '@antv/xflow';
import { DATASOURCE_NODE_RENDER_ID } from './constant';
import type { NsNodeCmd } from '@antv/xflow';
import type { NsNodeCollapsePanel } from '@antv/xflow';
import { Card } from 'antd';
export const onNodeDrop: NsNodeCollapsePanel.IOnNodeDrop = async (node, commands, modelService) => {
const args: NsNodeCmd.AddNode.IArgs = {
nodeConfig: { ...node, id: uuidv4() },
};
commands.executeCommand(XFlowNodeCommands.ADD_NODE.id, args);
};
const NodeDescription = (props: { name: string }) => {
return (
<Card size="small" style={{ width: '200px' }} bordered={false}>
</Card>
);
};
export const nodeDataService: NsNodeCollapsePanel.INodeDataService = async (meta, modelService) => {
return [
{
id: '数据源',
header: '数据源',
children: [
{
id: '2',
label: '新增数据源',
parentId: '1',
renderKey: DATASOURCE_NODE_RENDER_ID,
// renderComponent: (props) => (
// <div className="react-dnd-node react-custom-node-1"> {props.data.label} </div>
// ),
popoverContent: <NodeDescription name="数据源" />,
},
],
},
];
};
export const searchService: NsNodeCollapsePanel.ISearchService = async (
nodes: NsNodeCollapsePanel.IPanelNode[] = [],
keyword: string,
) => {
const list = nodes.filter((node) => node.label.includes(keyword));
return list;
};

View File

@@ -0,0 +1,79 @@
import type { IProps } from './index';
import { NsGraph } from '@antv/xflow';
import type { Graph } from '@antv/x6';
import { createHookConfig, DisposableCollection } from '@antv/xflow';
import { DATASOURCE_NODE_RENDER_ID, GROUP_NODE_RENDER_ID } from './constant';
import { AlgoNode } from './ReactNodes/algoNode';
import { GroupNode } from './ReactNodes/group';
export const useGraphHookConfig = createHookConfig<IProps>((config) => {
// 获取 Props
// const props = proxy.getValue();
config.setRegisterHook((hooks) => {
const disposableList = [
// 注册增加 react Node Render
hooks.reactNodeRender.registerHook({
name: 'add react node',
handler: async (renderMap) => {
renderMap.set(DATASOURCE_NODE_RENDER_ID, AlgoNode);
renderMap.set(GROUP_NODE_RENDER_ID, GroupNode);
},
}),
// 注册修改graphOptions配置的钩子
hooks.graphOptions.registerHook({
name: 'custom-x6-options',
after: 'dag-extension-x6-options',
handler: async (options) => {
const graphOptions: Graph.Options = {
connecting: {
allowLoop: false,
// 是否触发交互事件
validateMagnet() {
// return magnet.getAttribute('port-group') !== NsGraph.AnchorGroup.TOP
return true;
},
// 显示可用的链接桩
validateConnection(args: any) {
const { sourceView, targetView, sourceMagnet, targetMagnet } = args;
// 不允许连接到自己
if (sourceView === targetView) {
return false;
}
// 没有起点的返回false
if (!sourceMagnet) {
return false;
}
if (!targetMagnet) {
return false;
}
// 只能从上游节点的输出链接桩创建连接
if (sourceMagnet?.getAttribute('port-group') === NsGraph.AnchorGroup.LEFT) {
return false;
}
// 只能连接到下游节点的输入桩
if (targetMagnet?.getAttribute('port-group') === NsGraph.AnchorGroup.RIGHT) {
return false;
}
const node = targetView!.cell as any;
// 判断目标链接桩是否可连接
const portId = targetMagnet.getAttribute('port')!;
const port = node.getPort(portId);
return !!port;
},
},
};
options.connecting = { ...options.connecting, ...graphOptions.connecting };
},
}),
// hooks.afterGraphInit.registerHook({
// name: '注册toolTips工具',
// handler: async (args) => {},
// }),
];
const toDispose = new DisposableCollection();
toDispose.pushAll(disposableList);
return toDispose;
});
});

View File

@@ -0,0 +1,176 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import type { NsNodeCmd, NsEdgeCmd, IMenuOptions, NsGraph, NsGraphCmd } from '@antv/xflow';
import type { NsRenameNodeCmd } from './CmdExtensions/CmdRenameNodeModal';
import { createCtxMenuConfig, MenuItemType } from '@antv/xflow';
import { IconStore, XFlowNodeCommands, XFlowEdgeCommands, XFlowGraphCommands } from '@antv/xflow';
import { initDimensionGraphCmds } from './ConfigCmd';
import type { NsConfirmModalCmd } from './CmdExtensions/CmdConfirmModal';
import { NS_DATA_SOURCE_RELATION_MODAL_OPEN_STATE } from './ConfigModelService';
import { DeleteOutlined, EditOutlined, StopOutlined } from '@ant-design/icons';
import { CustomCommands } from './CmdExtensions/constants';
import { GraphApi } from './service';
/** menuitem 配置 */
export namespace NsMenuItemConfig {
/** 注册菜单依赖的icon */
IconStore.set('DeleteOutlined', DeleteOutlined);
IconStore.set('EditOutlined', EditOutlined);
IconStore.set('StopOutlined', StopOutlined);
export const DELETE_EDGE: IMenuOptions = {
id: XFlowEdgeCommands.DEL_EDGE.id,
label: '删除边',
iconName: 'DeleteOutlined',
onClick: async (args) => {
const { target, commandService, modelService } = args;
await commandService.executeCommand<NsEdgeCmd.DelEdge.IArgs>(XFlowEdgeCommands.DEL_EDGE.id, {
edgeConfig: target.data as NsGraph.IEdgeConfig,
});
// 保存数据源关联关系
await commandService.executeCommand(CustomCommands.DATASOURCE_RELATION.id, {});
// 保存图数据
commandService.executeCommand<NsGraphCmd.SaveGraphData.IArgs>(
XFlowGraphCommands.SAVE_GRAPH_DATA.id,
{ saveGraphDataService: (meta, graphData) => GraphApi.saveGraphData!(meta, graphData) },
);
// 关闭设置关联关系弹窗
const modalModel = await modelService!.awaitModel(
NS_DATA_SOURCE_RELATION_MODAL_OPEN_STATE.ID,
);
modalModel.setValue({ open: false });
},
};
export const DELETE_NODE: IMenuOptions = {
id: XFlowNodeCommands.DEL_NODE.id,
label: '删除节点',
iconName: 'DeleteOutlined',
onClick: async ({ target, commandService }) => {
commandService.executeCommand<NsNodeCmd.DelNode.IArgs>(XFlowNodeCommands.DEL_NODE.id, {
nodeConfig: { id: target?.data?.id || '', targetData: target.data },
});
},
};
export const EMPTY_MENU: IMenuOptions = {
id: 'EMPTY_MENU_ITEM',
label: '暂无可用',
isEnabled: false,
iconName: 'DeleteOutlined',
};
export const RENAME_NODE: IMenuOptions = {
id: CustomCommands.SHOW_RENAME_MODAL.id,
label: '重命名',
isVisible: true,
iconName: 'EditOutlined',
onClick: async ({ target, commandService }) => {
const nodeConfig = target.data as NsGraph.INodeConfig;
commandService.executeCommand<NsRenameNodeCmd.IArgs>(CustomCommands.SHOW_RENAME_MODAL.id, {
nodeConfig,
updateNodeNameService: GraphApi.renameNode,
});
},
};
export const DELETE_DATASOURCE_NODE: IMenuOptions = {
id: CustomCommands.SHOW_RENAME_MODAL.id,
label: '删除数据源',
isVisible: true,
iconName: 'EditOutlined',
onClick: async ({ target, commandService }) => {
const nodeConfig = {
...target.data,
modalProps: {
title: '确认删除?',
},
} as NsGraph.INodeConfig;
await commandService.executeCommand<NsConfirmModalCmd.IArgs>(
CustomCommands.SHOW_CONFIRM_MODAL.id,
{
nodeConfig,
confirmModalCallBack: async () => {
await commandService.executeCommand<NsNodeCmd.DelNode.IArgs>(
XFlowNodeCommands.DEL_NODE.id,
{
nodeConfig: {
id: target?.data?.id || '',
type: 'dataSource',
targetData: target.data,
},
},
);
commandService.executeCommand<NsGraphCmd.SaveGraphData.IArgs>(
XFlowGraphCommands.SAVE_GRAPH_DATA.id,
{
saveGraphDataService: (meta, graphData) => GraphApi.saveGraphData!(meta, graphData),
},
);
},
},
);
},
};
export const VIEW_DIMENSION: IMenuOptions = {
id: CustomCommands.VIEW_DIMENSION.id,
label: '查看维度',
isVisible: true,
iconName: 'EditOutlined',
onClick: async (args) => {
const { target, commandService, modelService } = args as any;
initDimensionGraphCmds({ commandService, target });
},
};
export const SEPARATOR: IMenuOptions = {
id: 'separator',
type: MenuItemType.Separator,
};
}
export const useMenuConfig = createCtxMenuConfig((config) => {
config.setMenuModelService(async (target, model, modelService, toDispose) => {
const { type, cell } = target as any;
switch (type) {
/** 节点菜单 */
case 'node':
model.setValue({
id: 'root',
type: MenuItemType.Root,
submenu: [
// NsMenuItemConfig.VIEW_DIMENSION,
// NsMenuItemConfig.SEPARATOR,
// NsMenuItemConfig.DELETE_NODE,
NsMenuItemConfig.DELETE_DATASOURCE_NODE,
// NsMenuItemConfig.RENAME_NODE,
],
});
break;
/** 边菜单 */
case 'edge':
model.setValue({
id: 'root',
type: MenuItemType.Root,
submenu: [NsMenuItemConfig.DELETE_EDGE],
});
break;
/** 画布菜单 */
case 'blank':
model.setValue({
id: 'root',
type: MenuItemType.Root,
submenu: [NsMenuItemConfig.EMPTY_MENU],
});
break;
/** 默认菜单 */
default:
model.setValue({
id: 'root',
type: MenuItemType.Root,
submenu: [NsMenuItemConfig.EMPTY_MENU],
});
break;
}
});
});

View File

@@ -0,0 +1,33 @@
import type { Disposable, IModelService } from '@antv/xflow';
import { createModelServiceConfig, DisposableCollection } from '@antv/xflow';
export namespace NS_DATA_SOURCE_RELATION_MODAL_OPEN_STATE {
export const ID = 'NS_DATA_SOURCE_RELATION_MODAL_OPEN_STATE';
// export const id = 'NS_DATA_SOURCE_RELATION_MODAL_OPEN_STATE';
export interface IState {
open: boolean;
}
}
export const useModelServiceConfig = createModelServiceConfig((config) => {
config.registerModel((registry) => {
const list: Disposable[] = [
registry.registerModel({
id: NS_DATA_SOURCE_RELATION_MODAL_OPEN_STATE.ID,
// getInitialValue: () => {
// open: false;
// },
}),
];
const toDispose = new DisposableCollection();
toDispose.pushAll(list);
return toDispose;
});
});
export const useOpenState = async (contextService: IModelService) => {
const ctx = await contextService.awaitModel<NS_DATA_SOURCE_RELATION_MODAL_OPEN_STATE.IState>(
NS_DATA_SOURCE_RELATION_MODAL_OPEN_STATE.ID,
);
return ctx.getValidValue();
};

View File

@@ -0,0 +1,242 @@
import type { IToolbarItemOptions } from '@antv/xflow';
import { createToolbarConfig } from '@antv/xflow';
import type { IModelService } from '@antv/xflow';
import {
XFlowGraphCommands,
XFlowDagCommands,
NsGraphStatusCommand,
MODELS,
IconStore,
} from '@antv/xflow';
import {
UngroupOutlined,
SaveOutlined,
CloudSyncOutlined,
GroupOutlined,
GatewayOutlined,
PlaySquareOutlined,
StopOutlined,
} from '@ant-design/icons';
import { GraphApi } from './service';
import type { NsGraphCmd } from '@antv/xflow';
import { Radio } from 'antd';
export namespace NSToolbarConfig {
/** 注册icon 类型 */
IconStore.set('SaveOutlined', SaveOutlined);
IconStore.set('CloudSyncOutlined', CloudSyncOutlined);
IconStore.set('GatewayOutlined', GatewayOutlined);
IconStore.set('GroupOutlined', GroupOutlined);
IconStore.set('UngroupOutlined', UngroupOutlined);
IconStore.set('PlaySquareOutlined', PlaySquareOutlined);
IconStore.set('StopOutlined', StopOutlined);
/** toolbar依赖的状态 */
export interface IToolbarState {
isMultiSelectionActive: boolean;
isNodeSelected: boolean;
isGroupSelected: boolean;
isProcessing: boolean;
}
export const getDependencies = async (modelService: IModelService) => {
return [
await MODELS.SELECTED_CELLS.getModel(modelService),
await MODELS.GRAPH_ENABLE_MULTI_SELECT.getModel(modelService),
await NsGraphStatusCommand.MODEL.getModel(modelService),
];
};
/** toolbar依赖的状态 */
export const getToolbarState = async (modelService: IModelService) => {
// isMultiSelectionActive
const { isEnable: isMultiSelectionActive } = await MODELS.GRAPH_ENABLE_MULTI_SELECT.useValue(
modelService,
);
// isGroupSelected
const isGroupSelected = await MODELS.IS_GROUP_SELECTED.useValue(modelService);
// isNormalNodesSelected: node不能是GroupNode
const isNormalNodesSelected = await MODELS.IS_NORMAL_NODES_SELECTED.useValue(modelService);
// statusInfo
const statusInfo = await NsGraphStatusCommand.MODEL.useValue(modelService);
return {
isNodeSelected: isNormalNodesSelected,
isGroupSelected,
isMultiSelectionActive,
isProcessing: statusInfo.graphStatus === NsGraphStatusCommand.StatusEnum.PROCESSING,
} as NSToolbarConfig.IToolbarState;
};
export const getToolbarItems = async () => {
const toolbarGroup1: IToolbarItemOptions[] = [];
const toolbarGroup2: IToolbarItemOptions[] = [];
const toolbarGroup3: IToolbarItemOptions[] = [];
/** 保存数据 */
toolbarGroup1.push({
id: XFlowGraphCommands.SAVE_GRAPH_DATA.id,
iconName: 'SaveOutlined',
tooltip: '保存数据',
onClick: async ({ commandService }) => {
commandService.executeCommand<NsGraphCmd.SaveGraphData.IArgs>(
XFlowGraphCommands.SAVE_GRAPH_DATA.id,
{ saveGraphDataService: (meta, graphData) => GraphApi.saveGraphData!(meta, graphData) },
);
},
});
// /** 部署服务按钮 */
// toolbarGroup1.push({
// iconName: 'CloudSyncOutlined',
// tooltip: '部署服务',
// id: CustomCommands.DEPLOY_SERVICE.id,
// onClick: ({ commandService }) => {
// commandService.executeCommand<NsDeployDagCmd.IArgs>(CustomCommands.DEPLOY_SERVICE.id, {
// deployDagService: (meta, graphData) => GraphApi.deployDagService(meta, graphData),
// });
// },
// });
// /** 开启框选 */
// toolbarGroup2.push({
// id: XFlowGraphCommands.GRAPH_TOGGLE_MULTI_SELECT.id,
// tooltip: '开启框选',
// iconName: 'GatewayOutlined',
// active: state.isMultiSelectionActive,
// onClick: async ({ commandService }) => {
// commandService.executeCommand<NsGraphCmd.GraphToggleMultiSelect.IArgs>(
// XFlowGraphCommands.GRAPH_TOGGLE_MULTI_SELECT.id,
// {},
// );
// },
// });
// /** 新建群组 */
// toolbarGroup2.push({
// id: XFlowGroupCommands.ADD_GROUP.id,
// tooltip: '新建群组',
// iconName: 'GroupOutlined',
// isEnabled: state.isNodeSelected,
// onClick: async ({ commandService, modelService }) => {
// const cells = await MODELS.SELECTED_CELLS.useValue(modelService);
// const groupChildren = cells.map((cell) => cell.id);
// commandService.executeCommand<NsGroupCmd.AddGroup.IArgs>(XFlowGroupCommands.ADD_GROUP.id, {
// nodeConfig: {
// id: uuidv4(),
// renderKey: GROUP_NODE_RENDER_ID,
// groupChildren,
// groupCollapsedSize: { width: 200, height: 40 },
// label: '新建群组',
// },
// });
// },
// });
// /** 解散群组 */
// toolbarGroup2.push({
// id: XFlowGroupCommands.DEL_GROUP.id,
// tooltip: '解散群组',
// iconName: 'UngroupOutlined',
// isEnabled: state.isGroupSelected,
// onClick: async ({ commandService, modelService }) => {
// const cell = await MODELS.SELECTED_NODE.useValue(modelService);
// const nodeConfig = cell.getData();
// commandService.executeCommand<NsGroupCmd.AddGroup.IArgs>(XFlowGroupCommands.DEL_GROUP.id, {
// nodeConfig: nodeConfig,
// });
// },
// });
// toolbarGroup3.push({
// id: XFlowDagCommands.QUERY_GRAPH_STATUS.id + 'play',
// tooltip: '开始执行',
// iconName: 'PlaySquareOutlined',
// isEnabled: !state.isProcessing,
// onClick: async ({ commandService }) => {
// commandService.executeCommand<NsGraphStatusCommand.IArgs>(
// XFlowDagCommands.QUERY_GRAPH_STATUS.id,
// {
// graphStatusService: GraphApi.graphStatusService,
// loopInterval: 3000,
// },
// );
// },
// });
// toolbarGroup3.push({
// id: XFlowDagCommands.QUERY_GRAPH_STATUS.id + 'stop',
// tooltip: '停止执行',
// iconName: 'StopOutlined',
// isEnabled: state.isProcessing,
// onClick: async ({ commandService }) => {
// commandService.executeCommand<NsGraphStatusCommand.IArgs>(
// XFlowDagCommands.QUERY_GRAPH_STATUS.id,
// {
// graphStatusService: GraphApi.stopGraphStatusService,
// loopInterval: 5000,
// },
// );
// },
// render: (props) => {
// return (
// <Popconfirm
// title="确定停止执行?"
// onConfirm={() => {
// props.onClick();
// }}
// >
// {props.children}
// </Popconfirm>
// );
// },
// });
return [
{ name: 'graphData', items: toolbarGroup1 },
{ name: 'groupOperations', items: toolbarGroup2 },
{
name: 'customCmd',
items: toolbarGroup3,
},
];
};
}
export const getExtraToolbarItems = async () => {
const toolbarGroup: IToolbarItemOptions[] = [];
/** 保存数据 */
toolbarGroup.push({
id: XFlowDagCommands.QUERY_GRAPH_STATUS.id + 'switchShowType',
render: () => {
return (
<Radio.Group defaultValue="dataSource" buttonStyle="solid" size="small">
<Radio.Button value="dataSource"></Radio.Button>
<Radio.Button value="dimension"></Radio.Button>
<Radio.Button value="metric"></Radio.Button>
</Radio.Group>
);
},
// text: '添加节点',
// tooltip: '添加节点配置extraGroups',
});
return [{ name: 'extra', items: toolbarGroup }];
};
export const useToolbarConfig = createToolbarConfig((toolbarConfig) => {
/** 生产 toolbar item */
toolbarConfig.setToolbarModelService(async (toolbarModel, modelService, toDispose) => {
const updateToolbarModel = async () => {
const state = await NSToolbarConfig.getToolbarState(modelService);
const toolbarItems = await NSToolbarConfig.getToolbarItems(state);
// const extraToolbarItems = await getExtraToolbarItems();
toolbarModel.setValue((toolbar) => {
toolbar.mainGroups = toolbarItems;
// toolbar.extraGroups = extraToolbarItems;
});
};
const models = await NSToolbarConfig.getDependencies(modelService);
const subscriptions = models.map((model) => {
return model.watch(async () => {
updateToolbarModel();
});
});
toDispose.pushAll(subscriptions);
});
});

View File

@@ -0,0 +1,85 @@
import React from 'react';
import ReactDom from 'react-dom';
import { Tooltip } from 'antd';
import type { EdgeView } from '@antv/x6';
import { Graph, ToolsView } from '@antv/x6';
class TooltipTool extends ToolsView.ToolItem<EdgeView, TooltipToolOptions> {
private knob: HTMLDivElement;
render() {
if (!this.knob) {
this.knob = ToolsView.createElement('div', false) as HTMLDivElement;
this.knob.style.position = 'absolute';
this.container.appendChild(this.knob);
}
return this;
}
private toggleTooltip(visible: boolean) {
if (this.knob) {
ReactDom.unmountComponentAtNode(this.knob);
if (visible) {
ReactDom.render(
<Tooltip title={this.options.tooltip} open={visible} destroyTooltipOnHide>
<div />
</Tooltip>,
this.knob,
);
}
}
}
private onMosueEnter({ e }: { e: MouseEvent }) {
this.updatePosition(e);
this.toggleTooltip(true);
}
private onMouseLeave() {
this.updatePosition();
this.toggleTooltip(false);
}
private onMouseMove() {
this.updatePosition();
this.toggleTooltip(false);
}
delegateEvents() {
this.cellView.on('cell:mouseenter', this.onMosueEnter, this);
this.cellView.on('cell:mouseleave', this.onMouseLeave, this);
this.cellView.on('cell:mousemove', this.onMouseMove, this);
return super.delegateEvents();
}
private updatePosition(e?: MouseEvent) {
const style = this.knob.style;
if (e) {
const p = this.graph.clientToGraph(e.clientX, e.clientY);
style.display = 'block';
style.left = `${p.x}px`;
style.top = `${p.y}px`;
} else {
style.display = 'none';
style.left = '-1000px';
style.top = '-1000px';
}
}
protected onRemove() {
this.toggleTooltip(false);
this.cellView.off('cell:mouseenter', this.onMosueEnter, this);
this.cellView.off('cell:mouseleave', this.onMouseLeave, this);
this.cellView.off('cell:mousemove', this.onMouseMove, this);
}
}
TooltipTool.config({
tagName: 'div',
isSVGElement: false,
});
export interface TooltipToolOptions extends ToolsView.ToolItem.Options {
tooltip?: string;
}
Graph.registerEdgeTool('tooltip', TooltipTool, true);

View File

@@ -0,0 +1,101 @@
@light-border: 1px solid #d9d9d9;
@primaryColor: #c2c8d5;
.xflow-algo-node {
z-index: 10;
display: flex;
width: 180px;
height: 36px;
line-height: 36px;
text-align: center;
background-color: #fff;
border: 1px solid @primaryColor;
border-radius: 2px;
box-shadow: ~'-1px -1px 4px 0 rgba(223,223,223,0.50), -2px 2px 4px 0 rgba(244,244,244,0.50), 2px 3px 8px 2px rgba(151,151,151,0.05)';
transition: all ease-in-out 0.15s;
&:hover {
background-color: #fff;
border: 1px solid #3057e3;
// border: 1px solid @primaryColor;
box-shadow: 0 0 3px 3px rgba(48, 86, 227, 0.15);
cursor: move;
}
.icon {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
}
.label {
width: 108px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-break: break-word;
}
.status {
width: 36px;
}
&.panel-node {
border: 0;
}
}
.x6-node-selected {
.xflow-algo-node {
background-color: rgba(48, 86, 227, 0.05);
border: 1px solid #3057e3;
box-shadow: 0 0 3px 3px rgba(48, 86, 227, 0.15);
&:hover {
background-color: #fff;
box-shadow: 0 0 5px 5px rgba(48, 86, 227, 0.15);
}
}
}
.dag-solution-layout {
.xflow-canvas-root {
.xflow-algo-node {
height: 72px !important;
line-height: 72px !important;
}
}
}
.dataSourceTooltipWrapper {
max-width: 500px;
.ant-tooltip-inner {
padding:0;
}
.dataSourceTooltip {
width: 300px;
background: #fff;
border-radius: 5px;
color: #4d4d4d;
padding: 15px;
opacity: .9;
font-size: 11px;
box-shadow: 0 0 5px #d8d8d8;
p {
margin-bottom: 0;
font-size: 13px;
padding: 4px;
line-height: 18px;
overflow: hidden;
display: flex;
flex-direction: row;
.dataSourceTooltipLabel {
display: block;
width: 64px;
color: grey;
}
.dataSourceTooltipValue{
flex: 1 1;
display: block;
width: 220px;
padding: 0 4px;
word-break: break-all;
}
}
}
}

View File

@@ -0,0 +1,112 @@
import React from 'react';
import {
DatabaseOutlined,
RedoOutlined,
CloseCircleOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
InfoCircleOutlined,
} from '@ant-design/icons';
import type { NsGraph } from '@antv/xflow';
import { NsGraphStatusCommand } from '@antv/xflow';
import { Tooltip } from 'antd';
import moment from 'moment';
import './algoNode.less';
const fontStyle = { fontSize: '16px', color: '#3057e3' };
interface IProps {
status: NsGraphStatusCommand.StatusEnum;
hide: boolean;
}
export const AlgoIcon: React.FC<IProps> = (props) => {
if (props.hide) {
return null;
}
switch (props.status) {
case NsGraphStatusCommand.StatusEnum.PROCESSING:
return <RedoOutlined spin style={{ color: '#c1cdf7', fontSize: '16px' }} />;
case NsGraphStatusCommand.StatusEnum.ERROR:
return <CloseCircleOutlined style={{ color: '#ff4d4f', fontSize: '16px' }} />;
case NsGraphStatusCommand.StatusEnum.SUCCESS:
return <CheckCircleOutlined style={{ color: '#39ca74cc', fontSize: '16px' }} />;
case NsGraphStatusCommand.StatusEnum.WARNING:
return <ExclamationCircleOutlined style={{ color: '#faad14', fontSize: '16px' }} />;
case NsGraphStatusCommand.StatusEnum.DEFAULT:
return <InfoCircleOutlined style={{ color: '#d9d9d9', fontSize: '16px' }} />;
default:
return null;
}
};
export const AlgoNode: NsGraph.INodeRender = (props) => {
const { data } = props;
const dataSourceData = data.payload;
const openState = dataSourceData ? undefined : false;
let tooltipNode = <></>;
if (dataSourceData) {
const { name, id, bizName, description, createdBy, updatedAt } = dataSourceData;
const labelList = [
{
label: '数据源ID',
value: id,
},
{
label: '名称',
value: name,
},
{
label: '英文名',
value: bizName,
},
{
label: '创建人',
value: createdBy,
},
{
label: '更新时间',
value: updatedAt ? moment(updatedAt).format('YYYY-MM-DD HH:mm:ss') : '-',
},
{
label: '描述',
value: description,
},
];
tooltipNode = (
<div className="dataSourceTooltip">
{labelList.map(({ label, value }) => {
return (
<p key={value}>
<span className="dataSourceTooltipLabel">{label}:</span>
<span className="dataSourceTooltipValue">{value || '-'}</span>
</p>
);
})}
</div>
);
}
return (
<div className={`xflow-algo-node ${props.isNodeTreePanel ? 'panel-node' : ''}`}>
<span className="icon">
<DatabaseOutlined style={fontStyle} />
</span>
<span className="label">
<Tooltip
open={openState}
title={tooltipNode}
placement="right"
color="#fff"
overlayClassName="dataSourceTooltipWrapper"
>
{props.data.label}
</Tooltip>
</span>
<span className="status">
<AlgoIcon status={props.data && props.data.status} hide={props.isNodeTreePanel} />
</span>
</div>
);
};

View File

@@ -0,0 +1,50 @@
@light-border: 1px solid #d9d9d9;
@primaryColor: #c1cdf7;
.xflow-group-node {
z-index: 9;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.65);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 4px;
box-shadow: ~'rgb(17 49 96 / 12%) 0px 1px 3px 0px, rgb(17 49 96 / 4%) 0px 0px 0px 1px';
cursor: grab;
&:hover {
background-color: rgba(227, 244, 255, 0.45);
border: 1px solid @primaryColor;
box-shadow: 0 0 3px 3px rgba(64, 169, 255, 0.2);
cursor: move;
}
.xflow-group-header {
display: flex;
justify-content: space-between;
padding: 0 12px;
font-size: 14px;
line-height: 38px;
.header-left {
width: 80%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.header-right {
display: inline-flex;
align-items: center;
span.anticon {
margin-left: 8px;
}
}
}
}
.x6-node-selected {
.xflow-group-node {
background-color: rgba(243, 249, 255, 0.92);
border: 1px solid @primaryColor;
box-shadow: 0 0 3px 3px rgb(64 169 255 / 20%);
&:hover {
background-color: rgba(243, 249, 255, 0.6);
}
}
}

View File

@@ -0,0 +1,37 @@
import { PlusSquareOutlined, MinusSquareOutlined } from '@ant-design/icons';
import type { NsGraph } from '@antv/xflow';
import { useXFlowApp, XFlowGroupCommands } from '@antv/xflow';
import './group.less';
export const GroupNode: NsGraph.INodeRender = (props) => {
const { cell } = props;
const app = useXFlowApp();
const isCollapsed = props.data.isCollapsed || false;
const onExpand = () => {
app.executeCommand(XFlowGroupCommands.COLLAPSE_GROUP.id, {
nodeId: cell.id,
isCollapsed: false,
collapsedSize: { width: 200, height: 40 },
});
};
const onCollapse = () => {
app.executeCommand(XFlowGroupCommands.COLLAPSE_GROUP.id, {
nodeId: cell.id,
isCollapsed: true,
collapsedSize: { width: 200, height: 40 },
gap: 3,
});
};
return (
<div className="xflow-group-node">
<div className="xflow-group-header">
<div className="header-left">{props.data.label}</div>
<div className="header-right">
{isCollapsed && <PlusSquareOutlined onClick={onExpand} />}
{!isCollapsed && <MinusSquareOutlined onClick={onCollapse} />}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,166 @@
import React, { useEffect, useState } from 'react';
import { Form, Button, Drawer, Space, Input, Select, message } from 'antd';
import { formLayout } from '@/components/FormHelper/utils';
import { createOrUpdateDatasourceRela } from '../../service';
import { getRelationConfigInfo } from '../utils';
import { useXFlowApp } from '@antv/xflow';
import { CustomCommands } from '../CmdExtensions/constants';
export type DataSourceRelationFormDrawerProps = {
domainId: number;
nodeDataSource: any;
open: boolean;
onClose?: () => void;
};
const FormItem = Form.Item;
const { Option } = Select;
const DataSourceRelationFormDrawer: React.FC<DataSourceRelationFormDrawerProps> = ({
domainId,
open,
nodeDataSource,
onClose,
}) => {
const [form] = Form.useForm();
const [saveLoading, setSaveLoading] = useState(false);
const [dataSourceOptions, setDataSourceOptions] = useState<any[]>([]);
const app = useXFlowApp();
const getRelationListInfo = async () => {
await app.commandService.executeCommand(CustomCommands.DATASOURCE_RELATION.id, {});
};
useEffect(() => {
const { sourceData, targetData } = nodeDataSource;
const dataSourceFromIdentifiers = sourceData?.datasourceDetail?.identifiers || [];
const dataSourceToIdentifiers = targetData?.datasourceDetail?.identifiers || [];
const dataSourceToIdentifiersNames = dataSourceToIdentifiers.map((item) => {
return item.name;
});
const keyOptions = dataSourceFromIdentifiers.reduce((options: any[], item: any) => {
const { name } = item;
if (dataSourceToIdentifiersNames.includes(name)) {
options.push(item);
}
return options;
}, []);
setDataSourceOptions(
keyOptions.map((item: any) => {
const { name } = item;
return {
label: name,
value: name,
};
}),
);
}, [nodeDataSource]);
useEffect(() => {
const { sourceData, targetData } = nodeDataSource;
if (!sourceData || !targetData) {
return;
}
const relationList = app.commandService.getGlobal('dataSourceRelationList') || [];
const config = getRelationConfigInfo(sourceData.id, targetData.id, relationList);
if (config) {
form.setFieldsValue({
joinKey: config.joinKey,
});
} else {
form.setFieldsValue({
joinKey: '',
});
}
}, [nodeDataSource]);
const renderContent = () => {
return (
<>
<FormItem hidden={true} name="id" label="ID">
<Input placeholder="id" />
</FormItem>
<FormItem label="主数据源:">{nodeDataSource?.sourceData?.name}</FormItem>
<FormItem label="关联数据源:">{nodeDataSource?.targetData?.name}</FormItem>
<FormItem
name="joinKey"
label="可关联Key:"
tooltip="主从数据源中必须具有相同的主键或外键才可建立关联关系"
rules={[{ required: true, message: '请选择关联Key' }]}
>
<Select placeholder="请选择关联Key">
{dataSourceOptions.map((item) => (
<Option key={item.value} value={item.value}>
{item.label}
</Option>
))}
</Select>
</FormItem>
</>
);
};
const saveRelation = async () => {
const values = await form.validateFields();
setSaveLoading(true);
const { code, msg } = await createOrUpdateDatasourceRela({
domainId,
datasourceFrom: nodeDataSource?.sourceData?.id,
datasourceTo: nodeDataSource?.targetData?.id,
...values,
});
setSaveLoading(false);
if (code === 200) {
message.success('保存成功');
getRelationListInfo();
onClose?.();
return;
}
message.error(msg);
};
const renderFooter = () => {
return (
<Space>
<Button
onClick={() => {
onClose?.();
}}
>
</Button>
<Button
type="primary"
loading={saveLoading}
onClick={() => {
saveRelation();
}}
>
</Button>
</Space>
);
};
return (
<Drawer
forceRender
width={400}
getContainer={false}
title={'数据源关联信息'}
mask={false}
open={open}
footer={renderFooter()}
onClose={() => {
onClose?.();
}}
>
<Form {...formLayout} form={form}>
{renderContent()}
</Form>
</Drawer>
);
};
export default DataSourceRelationFormDrawer;

View File

@@ -0,0 +1,35 @@
import React from 'react';
import { WorkspacePanel } from '@antv/xflow';
import type { NsJsonSchemaForm } from '@antv/xflow';
import XflowJsonSchemaFormDrawerForm from './XflowJsonSchemaFormDrawerForm';
export type CreateFormProps = {
controlMapService?: any;
formSchemaService?: any;
formValueUpdateService?: any;
};
const XflowJsonSchemaFormDrawer: React.FC<CreateFormProps> = ({
controlMapService,
formSchemaService,
formValueUpdateService,
}) => {
const defaultFormValueUpdateService: NsJsonSchemaForm.IFormValueUpdateService = async () => {};
const defaultFormSchemaService: NsJsonSchemaForm.IFormSchemaService = async () => {
return { tabs: [] };
};
const defaultControlMapService: NsJsonSchemaForm.IControlMapService = (controlMap) => {
return controlMap;
};
return (
<WorkspacePanel position={{}}>
<XflowJsonSchemaFormDrawerForm
controlMapService={controlMapService || defaultControlMapService}
formSchemaService={formSchemaService || defaultFormSchemaService}
formValueUpdateService={formValueUpdateService || defaultFormValueUpdateService}
/>
</WorkspacePanel>
);
};
export default XflowJsonSchemaFormDrawer;

View File

@@ -0,0 +1,125 @@
import React, { useEffect, useState } from 'react';
import { Drawer } from 'antd';
import { WorkspacePanel, useXFlowApp, useModelAsync, XFlowGraphCommands } from '@antv/xflow';
import { useJsonSchemaFormModel } from '@antv/xflow-extension/es/canvas-json-schema-form/service';
import { NS_DATA_SOURCE_RELATION_MODAL_OPEN_STATE } from '../ConfigModelService';
import { connect } from 'umi';
import { DATASOURCE_NODE_RENDER_ID } from '../constant';
import DataSourceRelationFormDrawer from './DataSourceRelationFormDrawer';
import { GraphApi } from '../service';
import type { StateType } from '../../model';
import DataSource from '../../Datasource';
export type CreateFormProps = {
controlMapService: any;
formSchemaService: any;
formValueUpdateService: any;
domainManger: StateType;
};
const XflowJsonSchemaFormDrawerForm: React.FC<CreateFormProps> = (props) => {
const { domainManger } = props;
const [visible, setVisible] = useState(false);
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
const [dataSourceItem, setDataSourceItem] = useState<any>();
const [nodeDataSource, setNodeDataSource] = useState<any>({
sourceData: {},
targetData: {},
});
const app = useXFlowApp();
// 借用JsonSchemaForm钩子函数对元素状态进行监听
const { state, commandService, modelService } = useJsonSchemaFormModel({
...props,
targetType: ['node', 'edge', 'canvas', 'group'],
position: {},
});
const [modalOpenState] = useModelAsync({
getModel: async () => {
return await modelService.awaitModel(NS_DATA_SOURCE_RELATION_MODAL_OPEN_STATE.ID);
},
initialState: false,
});
useEffect(() => {
const { open } = modalOpenState as any;
setVisible(open);
}, [modalOpenState]);
useEffect(() => {
const { targetType, targetData } = state;
if (targetType && ['node', 'edge'].includes(targetType)) {
const { renderKey, payload } = targetData as any;
if (renderKey === DATASOURCE_NODE_RENDER_ID) {
setDataSourceItem(payload);
setCreateModalVisible(true);
} else {
const { sourceNodeData, targetNodeData } = targetData as any;
setNodeDataSource({
sourceData: sourceNodeData.payload,
targetData: targetNodeData.payload,
});
setVisible(true);
}
}
}, [state]);
const resetSelectedNode = async () => {
const x6Graph = await app.graphProvider.getGraphInstance();
x6Graph.resetSelection();
};
const handleDataSourceRelationDrawerClose = () => {
resetSelectedNode();
setVisible(false);
};
return (
<WorkspacePanel position={{}}>
<DataSourceRelationFormDrawer
domainId={domainManger.selectDomainId}
nodeDataSource={nodeDataSource}
onClose={() => {
handleDataSourceRelationDrawerClose();
}}
open={visible}
/>
<Drawer
width={'100%'}
destroyOnClose
title="数据源编辑"
open={createModalVisible}
onClose={() => {
resetSelectedNode();
setCreateModalVisible(false);
setDataSourceItem(undefined);
}}
footer={null}
>
<DataSource
initialValues={dataSourceItem}
domainId={Number(domainManger?.selectDomainId)}
onSubmitSuccess={(dataSourceInfo: any) => {
setCreateModalVisible(false);
const { targetCell, targetData } = state;
targetCell?.setData({
...targetData,
label: dataSourceInfo.name,
payload: dataSourceInfo,
id: `dataSource-${dataSourceInfo.id}`,
});
setDataSourceItem(undefined);
commandService.executeCommand(XFlowGraphCommands.SAVE_GRAPH_DATA.id, {
saveGraphDataService: (meta, graphData) => GraphApi.saveGraphData!(meta, graphData),
});
}}
/>
</Drawer>
</WorkspacePanel>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(XflowJsonSchemaFormDrawerForm);

View File

@@ -0,0 +1,5 @@
export const DND_RENDER_ID = 'DND_NDOE';
export const GROUP_NODE_RENDER_ID = 'GROUP_NODE_RENDER_ID';
export const DATASOURCE_NODE_RENDER_ID = 'DATASOURCE_NODE';
export const NODE_WIDTH = 180;
export const NODE_HEIGHT = 72;

View File

@@ -0,0 +1,29 @@
import type { ISODateString, GraphConfigType, UserName } from '../data';
import type { NsGraph } from '@antv/xflow';
export type GraphConfigListItem = {
id: number;
domainId: number;
config: string;
type: GraphConfigType;
createdAt: ISODateString;
createdBy: UserName;
updatedAt: ISODateString;
updatedBy: UserName;
};
export type GraphConfig = { id: number; config: NsGraph.IGraphData };
export type RelationListItem = {
id: number;
domainId: number;
datasourceFrom: number;
datasourceTo: number;
joinKey: string;
createdAt: string;
createdBy: string;
updatedAt: string;
updatedBy: string;
};
export type RelationList = RelationListItem[];

View File

@@ -0,0 +1,103 @@
@body-bg: #fafafa;
@primaryColor: #3056e3;
@light-border: 1px solid #d9d9d9;
.dag-solution {
.__dumi-default-previewer-actions {
border: 0;
}
}
.dag-solution-layout {
position: relative;
height: 610px;
border: @light-border;
.xflow-x6-canvas {
background: @body-bg;
}
.x6-edge {
&:hover {
path:nth-child(2) {
stroke: @primaryColor;
stroke-width: 2px;
}
}
&.x6-edge-selected {
path:nth-child(2) {
stroke: @primaryColor;
stroke-width: 2px;
}
}
}
.xflow-canvas-dnd-node-tree {
border-right: @light-border;
}
.xflow-workspace-toolbar-top {
background-image: ~'linear-gradient(180deg, #ffffff 0%, #fafafa 100%)';
border-bottom: @light-border;
}
.xflow-workspace-toolbar-bottom {
text-align: center;
background: #fff;
border-top: @light-border;
}
.xflow-modal-container {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1000;
}
.xflow-collapse-panel {
.xflow-collapse-panel-header {
display: flex;
align-items: center;
justify-content: space-evenly;
background: #f7f8fa;
.ant-input-affix-wrapper {
padding: 2px 11px;
}
}
.xflow-collapse-panel-body {
background: #f7f8fa;
.xflow-collapse-header {
padding: 12px 8px;
}
}
.xflow-node-dnd-panel-footer {
display: none;
}
}
.xflow-json-form .tabs .ant-tabs-nav {
box-shadow: unset;
}
// .xflow-json-schema-form {
// .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
// color: #525252;
// font-weight: 300 !important;
// }
// .xflow-json-schema-form-footer {
// display: none;
// }
// .xflow-json-form .tabs.xTab .ant-tabs-nav .ant-tabs-nav-list,
// .xflow-json-form .tabs.xTab .ant-tabs-nav .ant-tabs-nav-list .ant-tabs-tab {
// background: #f7f8fa;
// }
// .xflow-json-schema-form-body {
// position: relative;
// width: 100%;
// height: 100%;
// background: #f7f8fa;
// box-shadow: 0 1px 1px 0 rgb(206 201 201 / 50%);
// }
// }
}

View File

@@ -0,0 +1,151 @@
import React, { useState, useEffect } from 'react';
/** app 核心组件 */
import { XFlow, XFlowCanvas, XFlowGraphCommands } from '@antv/xflow';
import type { IApplication, IAppLoad, NsGraph, NsGraphCmd } from '@antv/xflow';
/** 交互组件 */
import {
/** 触发Command的交互组件 */
CanvasScaleToolbar,
NodeCollapsePanel,
CanvasContextMenu,
CanvasToolbar,
/** Graph的扩展交互组件 */
CanvasSnapline,
CanvasNodePortTooltip,
DagGraphExtension,
} from '@antv/xflow';
/** app 组件配置 */
/** 配置画布 */
import { useGraphHookConfig } from './ConfigGraph';
/** 配置Command */
import { useCmdConfig, initGraphCmds } from './ConfigCmd';
/** 配置Model */
import { useModelServiceConfig } from './ConfigModelService';
/** 配置Menu */
import { useMenuConfig } from './ConfigMenu';
/** 配置Toolbar */
import { useToolbarConfig } from './ConfigToolbar';
/** 配置Dnd组件面板 */
import * as dndPanelConfig from './ConfigDndPanel';
import { connect } from 'umi';
import type { StateType } from '../model';
import './index.less';
import XflowJsonSchemaFormDrawer from './components/XflowJsonSchemaFormDrawer';
import { getViewInfoList } from '../service';
import { getGraphConfigFromList } from './utils';
import type { GraphConfig } from './data';
import '@antv/xflow/dist/index.css';
import './ReactNodes/ToolTipsNode';
export interface IProps {
domainManger: StateType;
}
export const SemanticFlow: React.FC<IProps> = (props) => {
const { domainManger } = props;
const graphHooksConfig = useGraphHookConfig(props);
const toolbarConfig = useToolbarConfig();
const menuConfig = useMenuConfig();
const cmdConfig = useCmdConfig();
const modelServiceConfig = useModelServiceConfig();
const [graphConfig, setGraphConfig] = useState<GraphConfig>();
const [meta, setMeta] = useState<NsGraph.IGraphMeta>({
flowId: 'semanticFlow',
domainManger,
});
const cache =
React.useMemo<{ app: IApplication } | null>(
() => ({
app: null as any,
}),
[],
) || ({} as any);
const queryGraphConfig = async () => {
const { code, data } = await getViewInfoList(domainManger.selectDomainId);
if (code === 200) {
const config = getGraphConfigFromList(data);
setGraphConfig(config || ({} as GraphConfig));
}
};
useEffect(() => {
queryGraphConfig();
}, [domainManger.selectDomainId]);
useEffect(() => {
setMeta({
...meta,
domainManger,
graphConfig,
});
}, [graphConfig]);
/**
* @param app 当前XFlow工作空间
*/
const onLoad: IAppLoad = async (app) => {
cache.app = app;
initGraphCmds(cache.app);
};
const updateGraph = async (app: IApplication) => {
await app.executeCommand(XFlowGraphCommands.LOAD_META.id, {
meta,
} as NsGraphCmd.GraphMeta.IArgs);
initGraphCmds(app);
};
/** 父组件meta属性更新时,执行initGraphCmds */
React.useEffect(() => {
if (cache.app) {
updateGraph(cache.app);
}
}, [cache.app, meta]);
return (
<div id="semanticFlowContainer" style={{ height: '100%' }}>
{meta.graphConfig && (
<XFlow
className="dag-user-custom-clz dag-solution-layout"
hookConfig={graphHooksConfig}
modelServiceConfig={modelServiceConfig}
commandConfig={cmdConfig}
onLoad={onLoad}
meta={meta}
>
<DagGraphExtension layout="LR" />
<NodeCollapsePanel
className="xflow-node-panel"
searchService={dndPanelConfig.searchService}
nodeDataService={dndPanelConfig.nodeDataService}
onNodeDrop={dndPanelConfig.onNodeDrop}
position={{ width: 230, top: 0, bottom: 0, left: 0 }}
footerPosition={{ height: 0 }}
bodyPosition={{ top: 40, bottom: 0, left: 0 }}
/>
<CanvasToolbar
className="xflow-workspace-toolbar-top"
layout="horizontal"
config={toolbarConfig}
position={{ top: 0, left: 230, right: 0, bottom: 0 }}
/>
<XFlowCanvas position={{ top: 40, left: 230, right: 0, bottom: 0 }}>
<CanvasScaleToolbar position={{ top: 60, left: 20 }} />
<CanvasContextMenu config={menuConfig} />
<CanvasSnapline color="#faad14" />
<CanvasNodePortTooltip />
</XFlowCanvas>
<XflowJsonSchemaFormDrawer />
</XFlow>
)}
</div>
);
};
export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger,
}))(SemanticFlow);

View File

@@ -0,0 +1,352 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { DATASOURCE_NODE_RENDER_ID, NODE_WIDTH, NODE_HEIGHT } from './constant';
import { uuidv4, NsGraph, NsGraphStatusCommand } from '@antv/xflow';
import type { NsRenameNodeCmd } from './CmdExtensions/CmdRenameNodeModal';
import type { NsNodeCmd, NsEdgeCmd, NsGraphCmd } from '@antv/xflow';
import type { NsDeployDagCmd } from './CmdExtensions/CmdDeploy';
import { getRelationConfigInfo, addClassInfoAsDataSourceParents } from './utils';
import { cloneDeep } from 'lodash';
import type { IDataSource } from '../data';
import {
getDatasourceList,
deleteDatasource,
getDimensionList,
createOrUpdateViewInfo,
getViewInfoList,
deleteDatasourceRela,
} from '../service';
import { message } from 'antd';
/** mock 后端接口调用 */
export namespace GraphApi {
export const NODE_COMMON_PROPS = {
renderKey: DATASOURCE_NODE_RENDER_ID,
width: NODE_WIDTH,
height: NODE_HEIGHT,
} as const;
/** 查图的meta元信息 */
export const queryGraphMeta: NsGraphCmd.GraphMeta.IArgs['graphMetaService'] = async (args) => {
return { ...args, flowId: args.meta.flowId };
};
export const createPorts = (nodeId: string, count = 1, layout = 'LR') => {
const ports = [] as NsGraph.INodeAnchor[];
Array(count)
.fill(1)
.forEach((item, idx) => {
const portIdx = idx + 1;
ports.push(
...[
{
id: `${nodeId}-input-${portIdx}`,
type: NsGraph.AnchorType.INPUT,
group: layout === 'TB' ? NsGraph.AnchorGroup.TOP : NsGraph.AnchorGroup.LEFT,
tooltip: `输入桩-${portIdx}`,
},
{
id: `${nodeId}-output-${portIdx}`,
type: NsGraph.AnchorType.OUTPUT,
group: layout === 'TB' ? NsGraph.AnchorGroup.BOTTOM : NsGraph.AnchorGroup.RIGHT,
tooltip: `输出桩-${portIdx}`,
},
],
);
});
return ports;
};
export const createDataSourceNode = (dataSourceItem: IDataSource.IDataSourceItem) => {
const { id, name } = dataSourceItem;
const nodeId = `dataSource-${id}`;
return {
...NODE_COMMON_PROPS,
id: nodeId,
label: `${name}-${id}`,
ports: createPorts(nodeId),
payload: dataSourceItem,
};
};
/** 删除节点的api */
export const delDataSource = async (nodeConfig: any) => {
const dataSourceId = nodeConfig.targetData?.payload?.id;
if (!dataSourceId) {
// dataSourceId 不存在时为未保存节点直接返回true删除
return true;
}
const { code, msg } = await deleteDatasource(dataSourceId);
if (code === 200) {
return true;
}
message.error(msg);
return false;
};
export const loadDataSourceData = async (args: NsGraph.IGraphMeta) => {
const { domainManger, graphConfig } = args.meta;
const { selectDomainId } = domainManger;
const { code, data = [] } = await getDatasourceList({ domainId: selectDomainId });
const dataSourceMap = data.reduce(
(itemMap: Record<string, IDataSource.IDataSourceItem>, item: IDataSource.IDataSourceItem) => {
const { id, name } = item;
itemMap[`dataSource-${id}`] = item;
itemMap[name] = item;
return itemMap;
},
{},
);
if (code === 200) {
// 如果config存在将数据源信息进行merge
if (graphConfig?.id && graphConfig?.config) {
const { config } = graphConfig;
const { nodes, edges } = config;
const nodesMap = nodes.reduce(
(itemMap: Record<string, NsGraph.INodeConfig>, item: NsGraph.INodeConfig) => {
itemMap[item.id] = item;
return itemMap;
},
{},
);
let mergeNodes = nodes;
let mergeEdges = edges;
if (Array.isArray(nodes)) {
mergeNodes = data.reduce(
(mergeNodeList: NsGraph.INodeConfig[], item: IDataSource.IDataSourceItem) => {
const { id } = item;
const targetDataSourceItem = nodesMap[`dataSource-${id}`];
if (targetDataSourceItem) {
mergeNodeList.push({
...targetDataSourceItem,
payload: item,
});
} else {
mergeNodeList.push(createDataSourceNode(item));
}
return mergeNodeList;
},
[],
);
}
if (Array.isArray(edges)) {
mergeEdges = edges.reduce(
(mergeEdgeList: NsGraph.IEdgeConfig[], item: NsGraph.IEdgeConfig) => {
const { source, target } = item;
const sourceDataSourceItem = dataSourceMap[source];
const targetDataSourceItem = dataSourceMap[target];
if (sourceDataSourceItem && targetDataSourceItem) {
const tempItem = { ...item };
tempItem.sourceNodeData.payload = sourceDataSourceItem;
tempItem.targetNodeData.payload = targetDataSourceItem;
mergeEdgeList.push(tempItem);
}
return mergeEdgeList;
},
[],
);
}
return { nodes: mergeNodes, edges: mergeEdges };
}
// 如果config不存在进行初始化
const nodes: NsGraph.INodeConfig[] = data.map((item: IDataSource.IDataSourceItem) => {
return createDataSourceNode(item);
});
return addClassInfoAsDataSourceParents({ nodes, edges: [] }, domainManger);
}
return {};
};
export const loadDimensionData = async (args: NsGraph.IGraphMeta) => {
const { domainManger } = args.meta;
const { domainId } = domainManger;
const { code, data } = await getDimensionList({ domainId });
if (code === 200) {
const { list } = data;
const nodes: NsGraph.INodeConfig[] = list.map((item: any) => {
const { id, name } = item;
const nodeId = `dimension-${id}`;
return {
...NODE_COMMON_PROPS,
id: nodeId,
label: `${name}-${id}`,
ports: createPorts(nodeId),
payload: item,
};
});
return { nodes, edges: [] };
}
return {};
};
/** 保存图数据的api */
export const saveGraphData: NsGraphCmd.SaveGraphData.IArgs['saveGraphDataService'] = async (
graphMeta: NsGraph.IGraphMeta,
graphData: NsGraph.IGraphData,
) => {
const { commandService } = graphMeta;
const initGraphCmdsState = commandService.getGlobal('initGraphCmdsSuccess');
// 如果graph处于初始化阶段则禁止配置文件保存操作
if (!initGraphCmdsState) {
return;
}
const tempGraphData = cloneDeep(graphData);
const { edges, nodes } = tempGraphData;
if (Array.isArray(nodes)) {
tempGraphData.nodes = nodes.map((item: any) => {
delete item.payload;
return item;
});
}
if (Array.isArray(edges)) {
tempGraphData.edges = edges.map((item: any) => {
delete item.sourceNodeData.payload;
delete item.targetNodeData.payload;
return item;
});
}
const { domainManger, graphConfig } = graphMeta.meta;
const { code, msg } = await createOrUpdateViewInfo({
id: graphConfig?.id,
domainId: domainManger.selectDomainId,
type: 'datasource',
config: JSON.stringify(tempGraphData),
});
if (code !== 200) {
message.error(msg);
}
return {
success: true,
data: graphData,
};
};
/** 部署图数据的api */
export const deployDagService: NsDeployDagCmd.IDeployDagService = async (
meta: NsGraph.IGraphMeta,
graphData: NsGraph.IGraphData,
) => {
return {
success: true,
data: graphData,
};
};
/** 添加节点api */
export const addNode: NsNodeCmd.AddNode.IArgs['createNodeService'] = async (
args: NsNodeCmd.AddNode.IArgs,
) => {
console.info('addNode service running, add node:', args);
const { id, ports = createPorts(id, 1), groupChildren } = args.nodeConfig;
const nodeId = id || uuidv4();
/** 这里添加连线桩 */
const node: NsNodeCmd.AddNode.IArgs['nodeConfig'] = {
...NODE_COMMON_PROPS,
...args.nodeConfig,
id: nodeId,
ports: ports,
};
/** group没有链接桩 */
if (groupChildren && groupChildren.length) {
node.ports = [];
}
return node;
};
/** 更新节点 name可能依赖接口判断是否重名返回空字符串时不更新 */
export const renameNode: NsRenameNodeCmd.IUpdateNodeNameService = async (
name,
node,
graphMeta,
) => {
return { err: null, nodeName: name };
};
/** 删除节点的api */
export const delNode: NsNodeCmd.DelNode.IArgs['deleteNodeService'] = async (args: any) => {
const { type } = args.nodeConfig;
switch (type) {
case 'dataSource':
return await delDataSource(args.nodeConfig);
case 'class':
return true;
default:
return true;
}
};
/** 添加边的api */
export const addEdge: NsEdgeCmd.AddEdge.IArgs['createEdgeService'] = async (args) => {
console.info('addEdge service running, add edge:', args);
const { edgeConfig } = args;
return {
...edgeConfig,
id: uuidv4(),
};
};
/** 删除边的api */
export const delEdge: NsEdgeCmd.DelEdge.IArgs['deleteEdgeService'] = async (args) => {
console.info('delEdge service running, del edge:', args);
const { commandService, edgeConfig } = args;
if (!edgeConfig?.sourceNodeData || !edgeConfig?.targetNodeData) {
return true;
}
const { sourceNodeData, targetNodeData } = edgeConfig as any;
const sourceDataId = sourceNodeData.payload.id;
const targetDataId = targetNodeData.payload.id;
const { getGlobal } = commandService as any;
const dataSourceRelationList = getGlobal('dataSourceRelationList');
const relationConfig = getRelationConfigInfo(
sourceDataId,
targetDataId,
dataSourceRelationList,
);
if (!relationConfig) {
// 如果配置不存在则直接删除
return true;
}
const { code, msg } = await deleteDatasourceRela(relationConfig.id);
if (code === 200) {
return true;
}
message.error(msg);
return false;
};
let runningNodeId = 0;
const statusMap = {} as NsGraphStatusCommand.IStatusInfo['statusMap'];
let graphStatus: NsGraphStatusCommand.StatusEnum = NsGraphStatusCommand.StatusEnum.DEFAULT;
export const graphStatusService: NsGraphStatusCommand.IArgs['graphStatusService'] = async () => {
if (runningNodeId < 4) {
statusMap[`node${runningNodeId}`] = { status: NsGraphStatusCommand.StatusEnum.SUCCESS };
statusMap[`node${runningNodeId + 1}`] = {
status: NsGraphStatusCommand.StatusEnum.PROCESSING,
};
runningNodeId += 1;
graphStatus = NsGraphStatusCommand.StatusEnum.PROCESSING;
} else {
runningNodeId = 0;
statusMap.node4 = { status: NsGraphStatusCommand.StatusEnum.SUCCESS };
graphStatus = NsGraphStatusCommand.StatusEnum.SUCCESS;
}
return {
graphStatus: graphStatus,
statusMap: statusMap,
};
};
export const stopGraphStatusService: NsGraphStatusCommand.IArgs['graphStatusService'] =
async () => {
Object.entries(statusMap).forEach(([, val]) => {
const { status } = val as { status: NsGraphStatusCommand.StatusEnum };
if (status === NsGraphStatusCommand.StatusEnum.PROCESSING) {
val.status = NsGraphStatusCommand.StatusEnum.ERROR;
}
});
return {
graphStatus: NsGraphStatusCommand.StatusEnum.ERROR,
statusMap: statusMap,
};
};
}

View File

@@ -0,0 +1,137 @@
import type { NsGraph } from '@antv/xflow';
import { uuidv4 } from '@antv/xflow';
import type { StateType } from '../model';
import { GraphApi } from './service';
import { NODE_WIDTH, NODE_HEIGHT } from './constant';
import moment from 'moment';
import { jsonParse } from '@/utils/utils';
import type { GraphConfigListItem, RelationListItem } from './data';
export const getEdgesNodesIds = (edges: NsGraph.IEdgeConfig[], type?: 'source' | 'target') => {
const hasEdgesNodesIds = edges.reduce((nodesList: string[], item: NsGraph.IEdgeConfig) => {
const { source, target } = item;
if (!type) {
nodesList.push(source, target);
} else if (type === 'source') {
nodesList.push(source);
} else if (type === 'target') {
nodesList.push(target);
}
return nodesList;
}, []);
const uniqueHasEdgesNodesIds = Array.from(new Set(hasEdgesNodesIds));
return uniqueHasEdgesNodesIds;
};
export const computedSingerNodesEdgesPosition = ({ nodes, edges }: NsGraph.IGraphData) => {
const hasEdgesNodesIds = getEdgesNodesIds(edges);
const defaultXPostion = 100;
const defaultYPostion = 100;
const paddingSize = 50;
let xPosistion = defaultXPostion;
const yPostition = defaultYPostion;
const positionNodes = nodes.reduce(
(nodesList: NsGraph.INodeConfig[], item: NsGraph.INodeConfig, index: number) => {
const { id, width, height = NODE_HEIGHT } = item;
if (!hasEdgesNodesIds.includes(id)) {
xPosistion = xPosistion + (width || NODE_WIDTH + paddingSize) * index;
}
nodesList.push({
...item,
x: xPosistion,
y: height > yPostition ? height + paddingSize : yPostition,
});
return nodesList;
},
[],
);
return { nodes: positionNodes, edges };
};
export const addClassInfoAsDataSourceParents = (
{ nodes = [], edges = [] }: NsGraph.IGraphData,
domainManger: StateType,
) => {
const { selectDomainId, selectDomainName } = domainManger;
const sourceId = `classNodeId-${selectDomainId}`;
const classNode = {
...GraphApi.NODE_COMMON_PROPS,
id: sourceId,
label: selectDomainName,
ports: GraphApi.createPorts(sourceId),
};
const classEdges = nodes.reduce((edgesList: NsGraph.IEdgeConfig[], item: NsGraph.INodeConfig) => {
const { id } = item;
const sourcePortId = `${sourceId}-output-1`;
const edge = {
id: uuidv4(),
source: sourceId,
target: id,
sourcePortId,
targetPortId: `${id}-input-1`,
};
edgesList.push(edge);
return edgesList;
}, []);
const graphData = {
nodes: [classNode, ...nodes],
edges: [...edges, ...classEdges],
};
return graphData;
};
export const addDataSourceInfoAsDimensionParents = (
{ nodes = [], edges = [] }: NsGraph.IGraphData,
targetDataSource: NsGraph.INodeConfig,
) => {
const { id: sourceId } = targetDataSource;
const dimensionEdges = nodes.reduce(
(edgesList: NsGraph.IEdgeConfig[], item: NsGraph.INodeConfig) => {
const { id } = item;
const sourcePortId = `${sourceId}-output-1`;
const edge = {
id: uuidv4(),
source: sourceId,
target: id,
sourcePortId,
targetPortId: `${id}-input-1`,
};
edgesList.push(edge);
return edgesList;
},
[],
);
const graphData = {
nodes: [targetDataSource, ...nodes],
edges: [...edges, ...dimensionEdges],
};
return graphData;
};
export const getGraphConfigFromList = (configList: GraphConfigListItem[]) => {
configList.sort((a, b) => moment(b.updatedAt).valueOf() - moment(a.updatedAt).valueOf());
const targetConfig = configList[0];
if (targetConfig) {
const { config, id } = targetConfig;
return {
config: jsonParse(config),
id,
};
}
return;
};
export const getRelationConfigInfo = (
fromDataSourceId: number,
toDataSourceId: number,
relationList: RelationListItem[],
) => {
const relationConfig = relationList.filter((item: RelationListItem) => {
const { datasourceFrom, datasourceTo } = item;
return fromDataSourceId === datasourceFrom && toDataSourceId === datasourceTo;
})[0];
return relationConfig;
};