瀏覽代碼

feat: add FlowHeader and delete edge (#959)

### What problem does this PR solve?
feat: add FlowHeader and delete edge #918 

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
tags/v0.7.0
balibabu 1 年之前
父節點
當前提交
495a6434ec
沒有連結到貢獻者的電子郵件帳戶。

+ 6
- 0
web/package-lock.json 查看文件

"classnames": "^2.5.1", "classnames": "^2.5.1",
"dagre": "^0.8.5", "dagre": "^0.8.5",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"elkjs": "^0.9.3",
"eventsource-parser": "^1.1.2", "eventsource-parser": "^1.1.2",
"i18next": "^23.7.16", "i18next": "^23.7.16",
"i18next-browser-languagedetector": "^8.0.0", "i18next-browser-languagedetector": "^8.0.0",
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.767.tgz", "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.767.tgz",
"integrity": "sha512-nzzHfmQqBss7CE3apQHkHjXW77+8w3ubGCIoEijKCJebPufREaFETgGXWTkh32t259F3Kcq+R8MZdFdOJROgYw==" "integrity": "sha512-nzzHfmQqBss7CE3apQHkHjXW77+8w3ubGCIoEijKCJebPufREaFETgGXWTkh32t259F3Kcq+R8MZdFdOJROgYw=="
}, },
"node_modules/elkjs": {
"version": "0.9.3",
"resolved": "https://registry.npmmirror.com/elkjs/-/elkjs-0.9.3.tgz",
"integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ=="
},
"node_modules/elliptic": { "node_modules/elliptic": {
"version": "6.5.5", "version": "6.5.5",
"resolved": "https://registry.npmmirror.com/elliptic/-/elliptic-6.5.5.tgz", "resolved": "https://registry.npmmirror.com/elliptic/-/elliptic-6.5.5.tgz",

+ 1
- 0
web/package.json 查看文件

"classnames": "^2.5.1", "classnames": "^2.5.1",
"dagre": "^0.8.5", "dagre": "^0.8.5",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"elkjs": "^0.9.3",
"eventsource-parser": "^1.1.2", "eventsource-parser": "^1.1.2",
"i18next": "^23.7.16", "i18next": "^23.7.16",
"i18next-browser-languagedetector": "^8.0.0", "i18next-browser-languagedetector": "^8.0.0",

+ 1
- 1
web/src/locales/zh-traditional.ts 查看文件

copied: '複製成功', copied: '複製成功',
comingSoon: '即將推出', comingSoon: '即將推出',
download: '下載', download: '下載',
close: '关闭',
close: '關閉',
preview: '預覽', preview: '預覽',
}, },
login: { login: {

+ 0
- 3
web/src/pages/flow/canvas/context-menu/index.tsx 查看文件

// event.clientY >= pane.height - 200 ? pane.height - event.clientY : 0, // event.clientY >= pane.height - 200 ? pane.height - event.clientY : 0,
// }); // });


console.info('clientX:', event.clientX);
console.info('clientY:', event.clientY);

setMenu({ setMenu({
id: node.id, id: node.id,
top: event.clientY - 72, top: event.clientY - 72,

+ 15
- 10
web/src/pages/flow/canvas/index.tsx 查看文件

import { NodeContextMenu, useHandleNodeContextMenu } from './context-menu'; import { NodeContextMenu, useHandleNodeContextMenu } from './context-menu';


import FlowDrawer from '../flow-drawer'; import FlowDrawer from '../flow-drawer';
import { useHandleDrop, useShowDrawer } from '../hooks';
import { initialEdges, initialNodes } from '../mock';
import { getLayoutedElements } from '../utils';
import {
useHandleDrop,
useHandleKeyUp,
useHandleSelectionChange,
useShowDrawer,
} from '../hooks';
import { dsl } from '../mock';
import { TextUpdaterNode } from './node'; import { TextUpdaterNode } from './node';


const nodeTypes = { textUpdater: TextUpdaterNode }; const nodeTypes = { textUpdater: TextUpdaterNode };
} }


function FlowCanvas({ sideWidth }: IProps) { function FlowCanvas({ sideWidth }: IProps) {
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
initialNodes,
initialEdges,
'LR',
);
const [nodes, setNodes] = useState<Node[]>(layoutedNodes);
const [edges, setEdges] = useState<Edge[]>(layoutedEdges);
const [nodes, setNodes] = useState<Node[]>(dsl.graph.nodes);
const [edges, setEdges] = useState<Edge[]>(dsl.graph.edges);

const { selectedEdges, selectedNodes } = useHandleSelectionChange();

const { ref, menu, onNodeContextMenu, onPaneClick } = const { ref, menu, onNodeContextMenu, onPaneClick } =
useHandleNodeContextMenu(sideWidth); useHandleNodeContextMenu(sideWidth);
const { drawerVisible, hideDrawer, showDrawer } = useShowDrawer(); const { drawerVisible, hideDrawer, showDrawer } = useShowDrawer();


const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop(setNodes); const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop(setNodes);


const { handleKeyUp } = useHandleKeyUp(selectedEdges, selectedNodes);

useEffect(() => { useEffect(() => {
console.info('nodes:', nodes); console.info('nodes:', nodes);
console.info('edges:', edges); console.info('edges:', edges);
onDragOver={onDragOver} onDragOver={onDragOver}
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
onInit={setReactFlowInstance} onInit={setReactFlowInstance}
onKeyUp={handleKeyUp}
> >
<Background /> <Background />
<Controls /> <Controls />

+ 35
- 0
web/src/pages/flow/elk-hooks.ts 查看文件

import { useCallback, useLayoutEffect } from 'react';
import { getLayoutedElements } from './elk-utils';

export const elkOptions = {
'elk.algorithm': 'layered',
'elk.layered.spacing.nodeNodeBetweenLayers': '100',
'elk.spacing.nodeNode': '80',
};

export const useLayoutGraph = (
initialNodes,
initialEdges,
setNodes,
setEdges,
) => {
const onLayout = useCallback(({ direction, useInitialNodes = false }) => {
const opts = { 'elk.direction': direction, ...elkOptions };
const ns = initialNodes;
const es = initialEdges;

getLayoutedElements(ns, es, opts).then(
({ nodes: layoutedNodes, edges: layoutedEdges }) => {
setNodes(layoutedNodes);
setEdges(layoutedEdges);

// window.requestAnimationFrame(() => fitView());
},
);
}, []);

// Calculate the initial layout on mount.
useLayoutEffect(() => {
onLayout({ direction: 'RIGHT', useInitialNodes: true });
}, [onLayout]);
};

+ 42
- 0
web/src/pages/flow/elk-utils.ts 查看文件

import ELK from 'elkjs/lib/elk.bundled.js';
import { Edge, Node } from 'reactflow';

const elk = new ELK();

export const getLayoutedElements = (
nodes: Node[],
edges: Edge[],
options = {},
) => {
const isHorizontal = options?.['elk.direction'] === 'RIGHT';
const graph = {
id: 'root',
layoutOptions: options,
children: nodes.map((node) => ({
...node,
// Adjust the target and source handle positions based on the layout
// direction.
targetPosition: isHorizontal ? 'left' : 'top',
sourcePosition: isHorizontal ? 'right' : 'bottom',

// Hardcode a width and height for elk to use when layouting.
width: 150,
height: 50,
})),
edges: edges,
};

return elk
.layout(graph)
.then((layoutedGraph) => ({
nodes: layoutedGraph.children.map((node) => ({
...node,
// React Flow expects a position property on the node instead of `x`
// and `y` fields.
position: { x: node.x, y: node.y },
})),

edges: layoutedGraph.edges,
}))
.catch(console.error);
};

+ 3
- 0
web/src/pages/flow/header/index.less 查看文件

.flowHeader {
padding: 20px;
}

+ 26
- 0
web/src/pages/flow/header/index.tsx 查看文件

import { Button, Flex } from 'antd';

import { useSaveGraph } from '../hooks';
import styles from './index.less';

const FlowHeader = () => {
const { saveGraph } = useSaveGraph();

return (
<Flex
align="center"
justify="end"
gap={'large'}
className={styles.flowHeader}
>
<Button>
<b>Debug</b>
</Button>
<Button type="primary" onClick={saveGraph}>
<b>Save</b>
</Button>
</Flex>
);
};

export default FlowHeader;

+ 63
- 2
web/src/pages/flow/hooks.ts 查看文件

import { useSetModalState } from '@/hooks/commonHooks'; import { useSetModalState } from '@/hooks/commonHooks';
import React, { Dispatch, SetStateAction, useCallback, useState } from 'react';
import { Node, Position, ReactFlowInstance } from 'reactflow';
import React, {
Dispatch,
KeyboardEventHandler,
SetStateAction,
useCallback,
useState,
} from 'react';
import {
Node,
Position,
ReactFlowInstance,
useOnSelectionChange,
useReactFlow,
} from 'reactflow';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';


export const useHandleDrag = () => { export const useHandleDrag = () => {
showDrawer, showDrawer,
}; };
}; };

export const useHandleSelectionChange = () => {
const [selectedNodes, setSelectedNodes] = useState<string[]>([]);
const [selectedEdges, setSelectedEdges] = useState<string[]>([]);

useOnSelectionChange({
onChange: ({ nodes, edges }) => {
setSelectedNodes(nodes.map((node) => node.id));
setSelectedEdges(edges.map((edge) => edge.id));
},
});

return { selectedEdges, selectedNodes };
};

export const useDeleteEdge = (selectedEdges: string[]) => {
const { setEdges } = useReactFlow();

const deleteEdge = useCallback(() => {
setEdges((edges) =>
edges.filter((edge) => selectedEdges.every((x) => x !== edge.id)),
);
}, [setEdges, selectedEdges]);

return deleteEdge;
};

export const useHandleKeyUp = (
selectedEdges: string[],
selectedNodes: string[],
) => {
const deleteEdge = useDeleteEdge(selectedEdges);
const handleKeyUp: KeyboardEventHandler = useCallback(
(e) => {
if (e.code === 'Delete') {
deleteEdge();
}
},
[deleteEdge],
);

return { handleKeyUp };
};

export const useSaveGraph = () => {
const saveGraph = useCallback(() => {}, []);

return { saveGraph };
};

+ 2
- 0
web/src/pages/flow/index.tsx 查看文件

import { ReactFlowProvider } from 'reactflow'; import { ReactFlowProvider } from 'reactflow';
import FlowCanvas from './canvas'; import FlowCanvas from './canvas';
import Sider from './flow-sider'; import Sider from './flow-sider';
import FlowHeader from './header';


const { Content } = Layout; const { Content } = Layout;


<ReactFlowProvider> <ReactFlowProvider>
<Sider setCollapsed={setCollapsed} collapsed={collapsed}></Sider> <Sider setCollapsed={setCollapsed} collapsed={collapsed}></Sider>
<Layout> <Layout>
<FlowHeader></FlowHeader>
<Content style={{ margin: '0 16px' }}> <Content style={{ margin: '0 16px' }}>
<FlowCanvas sideWidth={collapsed ? 0 : 200}></FlowCanvas> <FlowCanvas sideWidth={collapsed ? 0 : 200}></FlowCanvas>
</Content> </Content>

+ 94
- 0
web/src/pages/flow/mock.tsx 查看文件

]; ];


export const dsl = { export const dsl = {
graph: {
nodes: [
{
id: 'begin',
type: 'textUpdater',
position: {
x: 50,
y: 200,
},
data: {
label: 'Begin',
},
sourcePosition: 'left',
targetPosition: 'right',
},
{
id: 'Answer:China',
type: 'textUpdater',
position: {
x: 150,
y: 200,
},
data: {
label: 'Answer',
},
sourcePosition: 'left',
targetPosition: 'right',
},
{
id: 'Retrieval:China',
type: 'textUpdater',
position: {
x: 250,
y: 200,
},
data: {
label: 'Retrieval',
},
sourcePosition: 'left',
targetPosition: 'right',
},
{
id: 'Generate:China',
type: 'textUpdater',
position: {
x: 100,
y: 100,
},
data: {
label: 'Generate',
},
sourcePosition: 'left',
targetPosition: 'right',
},
],
edges: [
{
id: '7facb53d-65c9-43b3-ac55-339c445d3891',
label: '',
source: 'begin',
target: 'Answer:China',
markerEnd: {
type: 'arrow',
},
},
{
id: '7ac83631-502d-410f-a6e7-bec6866a5e99',
label: '',
source: 'Generate:China',
target: 'Answer:China',
markerEnd: {
type: 'arrow',
},
},
{
id: '0aaab297-5779-43ed-9281-2c4d3741566f',
label: '',
source: 'Answer:China',
target: 'Retrieval:China',
markerEnd: {
type: 'arrow',
},
},
{
id: '3477f9f3-0a7d-400e-af96-a11ea7673183',
label: '',
source: 'Retrieval:China',
target: 'Generate:China',
markerEnd: {
type: 'arrow',
},
},
],
},
components: { components: {
begin: { begin: {
obj: { obj: {

+ 5
- 2
web/src/pages/flow/utils.ts 查看文件

import { DSLComponents } from '@/interfaces/database/flow'; import { DSLComponents } from '@/interfaces/database/flow';
import dagre from 'dagre'; import dagre from 'dagre';
import { Edge, Node, Position } from 'reactflow';
import { Edge, MarkerType, Node, Position } from 'reactflow';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';


const buildEdges = ( const buildEdges = (
allEdges.push({ allEdges.push({
id: uuidv4(), id: uuidv4(),
label: '', label: '',
type: 'step',
// type: 'step',
source: source, source: source,
target: target, target: target,
markerEnd: {
type: MarkerType.Arrow,
},
}); });
} }
}); });

Loading…
取消
儲存