diff --git a/.dumirc.ts b/.dumirc.ts index de01295e2..ddb92e304 100644 --- a/.dumirc.ts +++ b/.dumirc.ts @@ -2,6 +2,7 @@ import { defineConfig } from 'dumi'; import { join } from 'path'; import { defineThemeConfig } from 'dumi-theme-antd/dist/defineThemeConfig'; import { Divider } from 'antd'; +import { dir } from 'console'; const isProduction = process.env.NODE_ENV === 'production'; const basePath = isProduction ? '/portal/' : '/'; @@ -15,12 +16,14 @@ const themeConfig = defineThemeConfig({ 'zh-CN': [ { title: '图组件', link: '/graphs' }, { title: '通用组件', link: '/components' }, + { title: '模型编辑', link: '/floweditors' }, { title: '建模', link: '/modelings' }, { title: '查询', link: '/queries' }, ], 'en-US': [ { title: 'graph', link: '/graphs' }, { title: 'components', link: '/components' }, + { title: 'flow-editor', link: '/floweditors' }, { title: 'modeling', link: '/modelings' }, { title: 'query', link: '/queries' }, ], @@ -181,6 +184,7 @@ export default defineConfig({ '@graphscope/studio-query': join(__dirname, 'packages', 'studio-query'), '@graphscope/use-zustand': join(__dirname, 'packages', 'use-zustand'), '@graphscope/studio-graph': join(__dirname, 'packages', 'studio-graph'), + '@graphscope/studio-flow-editor': join(__dirname, 'packages', 'studio-flow-editor'), }, externals: { 'node:os': 'commonjs2 node:os', @@ -205,6 +209,10 @@ export default defineConfig({ type: 'components/query-statement', dir: 'packages/studio-query/src/statement', }, + { + type: 'floweditor', + dir: 'packages/studio-flow-editor/docs', + }, { type: 'modeling', dir: 'packages/studio-importor/src/app', diff --git a/packages/studio-draw-pattern/.npmrc b/packages/studio-draw-pattern/.npmrc new file mode 100644 index 000000000..1d06f3bac --- /dev/null +++ b/packages/studio-draw-pattern/.npmrc @@ -0,0 +1 @@ +registry=http://registry.anpm.alibaba-inc.com diff --git a/packages/studio-draw-pattern/package.json b/packages/studio-draw-pattern/package.json index 6e9fb0442..b1227ff0c 100644 --- a/packages/studio-draw-pattern/package.json +++ b/packages/studio-draw-pattern/package.json @@ -38,11 +38,13 @@ "dependencies": { "@ant-design/icons": "^5.2.6", "@graphscope/studio-components": "workspace:*", + "@graphscope/studio-flow-editor": "workspace:*", "@graphscope/studio-graph-editor": "workspace:*", "antd": "^5.22.2", "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", + "reactflow": "^11.11.4", "rxjs": "^7.8.1", "uuid": "^10.0.0", "zustand": "^4.5.5" diff --git a/packages/studio-draw-pattern/src/components/Canvas/ButtonController.tsx b/packages/studio-draw-pattern/src/components/Canvas/ButtonController.tsx new file mode 100644 index 000000000..3cc3432c3 --- /dev/null +++ b/packages/studio-draw-pattern/src/components/Canvas/ButtonController.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Toolbar } from '@graphscope/studio-components'; +import { AddNode, ClearCanvas, ExportSvg } from '@graphscope/studio-flow-editor'; + +const ButtonController: React.FunctionComponent = props => { + return ( + + + + + + ); +}; + +export default ButtonController; diff --git a/packages/studio-draw-pattern/src/components/Canvas/index.tsx b/packages/studio-draw-pattern/src/components/Canvas/index.tsx index 2934a642a..d74e3781d 100644 --- a/packages/studio-draw-pattern/src/components/Canvas/index.tsx +++ b/packages/studio-draw-pattern/src/components/Canvas/index.tsx @@ -1,21 +1,21 @@ -import { Graph } from '@graphscope/studio-graph-editor'; import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { useNodeStore } from '../../stores/useNodeStore'; import { useEdgeStore } from '../../stores/useEdgeStore'; -import { ISchemaEdge } from '@graphscope/studio-graph-editor'; import { useGenerateRelation } from '../../hooks/generateRelation/useGenerateRelation'; -import PopoverContent from './PopoverContent'; import { Property } from '../../types/property'; import { useTransform } from '../../hooks/transform/useTransform'; -import { ISchemaNode } from '@graphscope/studio-graph-editor'; import { useGraphStore } from '../../stores/useGraphStore'; import { useEncodeCypher } from '../../hooks/cypher/useEncodeCypher'; -import { Button, Input, Modal, Tooltip } from 'antd'; +import { Button, Tooltip } from 'antd'; import { usePropertiesStore } from '../../stores/usePropertiesStore'; +import ButtonController from './ButtonController'; import { DrawPatternContext, DrawPatternValue } from '../DrawPattern'; import _ from 'lodash'; +import { theme } from 'antd'; import { useSection } from '@graphscope/studio-components'; +import { Background, MiniMap, Controls } from 'reactflow'; import { InsertRowRightOutlined, SearchOutlined } from '@ant-design/icons'; +import { GraphProvider, GraphCanvas, ISchemaEdge, ISchemaNode } from '@graphscope/studio-flow-editor'; export const Canvas = () => { const [descState, setDescState] = useState(); @@ -40,6 +40,7 @@ export const Canvas = () => { const { toggleLeftSide } = useSection(); const [clickTrigger, setClickTrigger] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); + const { token } = theme.useToken(); useEffect(() => { const nodeReturn = Array.from(nodesStore).map(node => `${node.variable}`); @@ -115,22 +116,32 @@ export const Canvas = () => { const MyGraph = useCallback(() => { return ( - } - /> + > + + + + + ); }, [graphNodes, graphEdges]); return (
- + + +
{ flexBasis: '50%', marginTop: '1rem', overflow: 'hidden', + position: 'relative', }} > Model Preview
- + + +
); diff --git a/packages/studio-draw-pattern/src/hooks/transform/useTransform.ts b/packages/studio-draw-pattern/src/hooks/transform/useTransform.ts index 2c7b011e5..3349d9d5f 100644 --- a/packages/studio-draw-pattern/src/hooks/transform/useTransform.ts +++ b/packages/studio-draw-pattern/src/hooks/transform/useTransform.ts @@ -1,7 +1,6 @@ -import { ISchemaNode } from '@graphscope/studio-graph-editor'; +import { ISchemaEdge, ISchemaNode} from '@graphscope/studio-flow-editor'; import { useCallback } from 'react'; import { Node } from '../../types/node'; -import { ISchemaEdge } from '@graphscope/studio-graph-editor'; import { Edge } from '../../types/edge'; import { useNodeStore } from '../../stores/useNodeStore'; import { useEdgeStore } from '../../stores/useEdgeStore'; diff --git a/packages/studio-draw-pattern/src/stores/useGraphStore.ts b/packages/studio-draw-pattern/src/stores/useGraphStore.ts index 4ee01451d..3f4769c1e 100644 --- a/packages/studio-draw-pattern/src/stores/useGraphStore.ts +++ b/packages/studio-draw-pattern/src/stores/useGraphStore.ts @@ -1,5 +1,4 @@ -import { ISchemaEdge } from '@graphscope/studio-graph-editor'; -import { ISchemaNode } from '@graphscope/studio-graph-editor'; +import { ISchemaEdge, ISchemaNode} from '@graphscope/studio-flow-editor'; import _ from 'lodash'; import { create } from 'zustand'; diff --git a/packages/studio-draw-pattern/src/types/edge.d.ts b/packages/studio-draw-pattern/src/types/edge.d.ts index 09ec7316e..327423221 100644 --- a/packages/studio-draw-pattern/src/types/edge.d.ts +++ b/packages/studio-draw-pattern/src/types/edge.d.ts @@ -1,4 +1,4 @@ -import { IEdgeData, ISchemaEdge } from '@graphscope/studio-graph-editor'; +import { IEdgeData, ISchemaNode} from '@graphscope/studio-flow-editor'; import { Property } from './property'; export interface EdgeData extends ISchemaEdge { diff --git a/packages/studio-draw-pattern/src/types/node.d.ts b/packages/studio-draw-pattern/src/types/node.d.ts index 819255e82..4423eaff7 100644 --- a/packages/studio-draw-pattern/src/types/node.d.ts +++ b/packages/studio-draw-pattern/src/types/node.d.ts @@ -1,4 +1,4 @@ -import { ISchemaNode } from '@graphscope/studio-graph-editor'; +import { ISchemaNode } from '@graphscope/studio-flow-editor'; import type { Property } from './property'; export interface NodeData extends ISchemaNode { diff --git a/packages/studio-draw-pattern/src/utils/index.ts b/packages/studio-draw-pattern/src/utils/index.ts index 2e215dd8b..23090069c 100644 --- a/packages/studio-draw-pattern/src/utils/index.ts +++ b/packages/studio-draw-pattern/src/utils/index.ts @@ -1,4 +1,4 @@ -import { ISchemaNode } from '@graphscope/studio-graph-editor'; +import { ISchemaNode } from '@graphscope/studio-flow-editor'; export function isArrayExist(item: T, arrary: Array): boolean { return arrary.indexOf(item) !== -1; diff --git a/packages/studio-flow-editor/.fatherrc.js b/packages/studio-flow-editor/.fatherrc.js new file mode 100644 index 000000000..c4f694d60 --- /dev/null +++ b/packages/studio-flow-editor/.fatherrc.js @@ -0,0 +1,4 @@ +export default { + esm: { output: 'es' }, + cjs: { output: 'lib' }, +}; diff --git a/packages/studio-flow-editor/.npmrc b/packages/studio-flow-editor/.npmrc new file mode 100644 index 000000000..6ab414d06 --- /dev/null +++ b/packages/studio-flow-editor/.npmrc @@ -0,0 +1 @@ +registry=http://registry.anpm.alibaba-inc.com \ No newline at end of file diff --git a/packages/studio-flow-editor/CHANGELOG.md b/packages/studio-flow-editor/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/packages/studio-flow-editor/README.md b/packages/studio-flow-editor/README.md new file mode 100644 index 000000000..6eb97d628 --- /dev/null +++ b/packages/studio-flow-editor/README.md @@ -0,0 +1,403 @@ +# Studio Flow Editor + +## 概述 + +`@graphscope/studio-flow-editor` 是一个基于 React 的图编辑器组件库,提供了一套完整的图数据可视化和交互编辑解决方案。该库基于 ReactFlow 构建,支持节点与边的直观编辑、自动布局以及高度可定制的图形表示。 + +## 主要特性 + +- **交互式编辑**:支持拖拽式创建、移动节点和连线 +- **状态管理**:集成 Zustand 进行高效状态管理 +- **多实例支持**:允许在同一页面创建多个独立图编辑器 +- **自动布局**:内置力导向图布局算法 +- **类型安全**:完整的 TypeScript 类型定义 +- **自定义外观**:可定制节点和边的样式与行为 +- **导出功能**:支持导出为 SVG 图片 + +## 安装 + +```bash +# 使用npm +npm install @graphscope/studio-flow-editor + +# 使用yarn +yarn add @graphscope/studio-flow-editor + +# 使用pnpm +pnpm add @graphscope/studio-flow-editor +``` + +## 基本用法 + +下面是一个最简单的例子,展示了如何创建一个基本的图编辑器: + +```jsx +import React from 'react'; +import { GraphProvider, GraphCanvas } from '@graphscope/studio-flow-editor'; + +const App = () => { + return ( +
+ + + +
+ ); +}; + +export default App; +``` + +## 主要组件 + +### GraphProvider + +状态管理提供者,负责管理图编辑器的所有状态。 + +```jsx + + {children} + +``` + +#### 属性 + +| 属性名 | 类型 | 必填 | 默认值 | 描述 | +|-----------|-----------------|------|--------|----------------------------| +| id | string | 否 | 自动生成 | 图实例ID,用于多实例管理 | +| children | React.ReactNode | 是 | - | 子组件 | + +### GraphCanvas + +主编辑器组件,提供可视化图编辑功能。 + +```jsx + console.log(nodes)} + onEdgesChange={(edges) => console.log(edges)} + onSelectionChange={(nodes, edges) => console.log('selection changed')} + noDefaultLabel={false} + defaultNodes={[]} + defaultEdges={[]} +> + {/* 自定义子组件 */} + +``` + +#### 属性 + +| 属性名 | 类型 | 必填 | 默认值 | 描述 | +|-------------------|------------------------------------------------------|------|---------|----------------------------| +| children | React.ReactNode | 否 | - | 子组件 | +| nodesDraggable | boolean | 否 | true | 节点是否可拖拽 | +| isPreview | boolean | 否 | false | 是否为预览模式 | +| onNodesChange | (nodes: ISchemaNode[]) => void | 否 | - | 节点变化回调 | +| onEdgesChange | (edges: ISchemaEdge[]) => void | 否 | - | 边变化回调 | +| onSelectionChange | (nodes: ISchemaNode[], edges: ISchemaEdge[]) => void | 否 | - | 选择变化回调 | +| noDefaultLabel | boolean | 否 | false | 是否禁用默认标签 | +| defaultNodes | ISchemaNode[] | 否 | [] | 初始节点 | +| defaultEdges | ISchemaEdge[] | 否 | [] | 初始边 | + +## 工具组件 + +### AddNode + +添加新节点的按钮组件。 + +```jsx +import { AddNode } from '@graphscope/studio-flow-editor'; + +// 基本用法 + + +// 自定义样式 + +``` + +#### 属性 + +| 属性名 | 类型 | 必填 | 默认值 | 描述 | +|--------|---------------------|------|--------|--------------| +| style | React.CSSProperties | 否 | - | 内联样式对象 | + +### ClearCanvas + +清除画布或删除选中元素的按钮组件。 + +```jsx +import { ClearCanvas } from '@graphscope/studio-flow-editor'; + +// 基本用法 + + +// 自定义样式 + +``` + +#### 属性 + +| 属性名 | 类型 | 必填 | 默认值 | 描述 | +|--------|---------------------|------|--------|--------------| +| style | React.CSSProperties | 否 | - | 内联样式对象 | + +### ExportSvg + +导出图为SVG文件的按钮组件。 + +```jsx +import { ExportSvg } from '@graphscope/studio-flow-editor'; + +// 基本用法 + + +// 自定义样式和文件名 + +``` + +#### 属性 + +| 属性名 | 类型 | 必填 | 默认值 | 描述 | +|----------|---------------------|------|------------|------------------------------------------| +| style | React.CSSProperties | 否 | - | 内联样式对象 | +| fileName | string | 否 | 'graph.svg' | 导出文件名 | +| parentId | string | 否 | - | 存在多个图形实例时,指定导出图上层dom的id | + +## Hooks + +### useGraphStore + +访问和更新图状态的主要钩子。 + +```jsx +import { useGraphStore } from '@graphscope/studio-flow-editor'; + +// 获取状态和更新函数 +const { store, updateStore } = useGraphStore(); + +// 读取状态 +const { nodes, edges, currentId } = store; + +// 更新状态 +updateStore(draft => { + draft.nodes.push(newNode); +}); + +// 使用选择器优化性能 +const nodes = useGraphStore(state => state.nodes); +``` + +#### 状态结构 + +```typescript +interface GraphState { + displayMode: 'graph' | 'table'; // 显示模式 + nodes: ISchemaNode[]; // 节点列表 + edges: ISchemaEdge[]; // 边列表 + nodePositionChange: NodePositionChange[]; // 节点位置变更 + hasLayouted: boolean; // 是否已布局 + elementOptions: { // 元素选项 + isEditable: boolean; // 是否可编辑 + isConnectable: boolean; // 是否可连接 + }; + theme: { // 主题 + primaryColor: string; // 主色调 + }; + currentId: string; // 当前选中元素ID + currentType: 'nodes' | 'edges'; // 当前选中元素类型 + selectedNodeIds: string[]; // 选中的节点ID数组 + selectedEdgeIds: string[]; // 选中的边ID数组 +} +``` + +### useClearCanvas + +清空画布或删除选中元素的钩子。 + +```jsx +import { useClearCanvas } from '@graphscope/studio-flow-editor'; + +const { handleClear } = useClearCanvas(); + +// 清空画布或删除选中元素 +handleClear(); +``` + +### useAddNode + +添加新节点到画布的钩子。 + +```jsx +import { useAddNode } from '@graphscope/studio-flow-editor'; + +const { handleAddVertex } = useAddNode(); + +// 在指定位置添加节点 +handleAddVertex({ x: 100, y: 100 }); +``` + +### useExportSvg + +导出图为SVG或图片的钩子。 + +```jsx +import { useExportSvg } from '@graphscope/studio-flow-editor'; + +const { exportSvg } = useExportSvg(); + +// 导出为SVG +exportSvg({ name: 'graph.svg' }); +``` + +## 工具函数 + +### 布局工具 + +```jsx +import { getBBox } from '@graphscope/studio-flow-editor'; + +// 获取节点边界框 +const bbox = getBBox(nodes); +// 返回: { x, y, width, height } +``` + +### 标签工具 + +```jsx +import { createNodeLabel, createEdgeLabel, resetIndex } from '@graphscope/studio-flow-editor'; + +// 创建节点标签(自动递增 - Vertex_1, Vertex_2, ...) +const nodeLabel = createNodeLabel(); + +// 创建边标签(自动递增 - Edge_1, Edge_2, ...) +const edgeLabel = createEdgeLabel(); + +// 重置标签索引 +resetIndex(); +``` + +### 数据处理 + +```jsx +import { fakeSnapshot } from '@graphscope/studio-flow-editor'; + +// 创建数据快照(深拷贝) +const snapshot = fakeSnapshot(data); +``` + +## 类型定义 + +```typescript +// 节点数据类型 +interface INodeData { + label: string; + disabled?: boolean; + properties?: Property[]; + dataFields?: string[]; + delimiter?: string; + datatype?: 'csv' | 'odps'; + filelocation?: string; + [key: string]: any; +} + +// 边数据类型 +interface IEdgeData { + label: string; + disabled?: boolean; + saved?: boolean; + properties?: Property[]; + source_vertex_fields?: Property; + target_vertex_fields?: Property; + dataFields?: string[]; + delimiter?: string; + datatype?: 'csv' | 'odps'; + filelocation?: string; + _extra?: { + type?: string; + offset?: string; + isLoop: boolean; + isRevert?: boolean; + isPoly?: boolean; + index?: number; + count?: number; + }; + [key: string]: any; +} + +// 节点和边的类型 +type ISchemaNode = Node; +type ISchemaEdge = Edge & { data: IEdgeData }; +``` + +## 组合使用示例 + +这些组件通常组合在一起使用,创建一个完整的工具栏: + +```jsx +import React from 'react'; +import { + GraphProvider, + GraphCanvas, + AddNode, + ClearCanvas, + ExportSvg +} from '@graphscope/studio-flow-editor'; + +const ToolbarStyle = { + position: 'absolute', + top: '10px', + right: '10px', + zIndex: 10, + display: 'flex', + gap: '8px', + background: 'white', + padding: '8px', + borderRadius: '4px', + boxShadow: '0 2px 6px rgba(0,0,0,0.1)' +}; + +const App = () => { + return ( +
+ + +
+ + + +
+
+
+
+ ); +}; + +export default App; +``` + +## 多实例支持 + +您可以通过指定不同的 `id` 在同一页面上创建多个独立的流程图实例: + +```jsx +
+
+ + + +
+ +
+ + + +
+
+``` + + diff --git a/packages/studio-flow-editor/docs/GraphCanvas.md b/packages/studio-flow-editor/docs/GraphCanvas.md new file mode 100644 index 000000000..a8ff4d09f --- /dev/null +++ b/packages/studio-flow-editor/docs/GraphCanvas.md @@ -0,0 +1,369 @@ +--- +order: 3 +title: GraphCanvas +--- + +# GraphCanvas 组件 + +`GraphCanvas` 组件是 `@graphscope/studio-flow-editor` 包的核心组件,提供了完整的图形可视化和交互编辑功能。本文档详细介绍该组件的用法和配置选项。 + +## 导入方式 + +```bash +import { GraphCanvas } from '@graphscope/studio-flow-editor'; +``` + +## 基本用法 + +```bash +import React from 'react'; +import { GraphProvider, GraphCanvas } from '@graphscope/studio-flow-editor'; + +const MyGraph = () => { + return ( + + + {/* 可在此添加自定义UI组件 */} + + + ); +}; +``` + +## 属性配置 + +| 属性名 | 类型 | 默认值 | 说明 | +| ------------------- | ------------- | ------- | -------------------------------------- | +| `children` | ReactNode | - | 可以在编辑器内部渲染的自定义UI元素 | +| `nodesDraggable` | boolean | `true` | 是否允许拖拽节点 | +| `isPreview` | boolean | `false` | 是否为预览模式,预览模式下禁用编辑功能 | +| `onNodesChange` | function | - | 节点变化时的回调函数 | +| `onEdgesChange` | function | - | 边变化时的回调函数 | +| `onSelectionChange` | function | - | 选择变化时的回调函数 | +| `noDefaultLabel` | boolean | - | 是否禁用默认标签生成 | +| `defaultNodes` | ISchemaNode[] | - | 初始节点列表 | +| `defaultEdges` | ISchemaEdge[] | - | 初始边列表 | + +## 功能特性 + +`GraphCanvas` 组件提供以下核心功能: + +1. **节点管理** + + - 点击或拖拽创建节点 + - 拖拽移动节点位置 + - 键盘或右键菜单删除节点 + - 使用Shift+点击或框选多选节点 + +2. **边管理** + + - 从一个节点拖拽到另一个节点创建边 + - 编辑边的属性和标签 + - 键盘或右键菜单删除边 + - 支持自环边和多边 + +3. **画布导航** + + - 拖拽背景平移视图 + - 鼠标滚轮缩放 + - 双击背景适应视图 + +4. **布局功能** + - 自动力导向布局初始定位 + - 手动调整节点位置 + - 支持自定义布局配置 + +## 内部组件结构 + +`GraphCanvas` 组件内部由多个组件组合而成: + +- **GraphCanvas**:主要的画布区域,包含节点和边 +- **ReactFlow**:底层的 ReactFlow 组件,负责图形渲染 +- **ArrowMarker**:定义边的箭头样式 +- **ConnectionLine**:创建新连接时显示的连接线样式 + +## 键盘快捷键 + +- **Delete/Backspace**:删除选中的节点/边 +- **Escape**:取消当前操作/选择 + +## 与 GraphProvider 集成 + +`GraphCanvas` 必须在 `GraphProvider` 内使用才能访问共享状态: + +```bash + + + +``` + + +## 添加初始数据 + +可以通过 `defaultNodes` 和 `defaultEdges` 属性为编辑器提供初始数据: + +```jsx +import React from 'react'; +import { GraphProvider, GraphCanvas } from '@graphscope/studio-flow-editor'; + +const App = () => { + // 初始节点数据 + const initialNodes = [ + { + id: 'node-1', + type: 'graph-node', + position: { x: 100, y: 100 }, + data: { label: '节点1' }, + }, + { + id: 'node-2', + type: 'graph-node', + position: { x: 300, y: 200 }, + data: { label: '节点2' }, + }, + ]; + + // 初始边数据 + const initialEdges = [ + { + id: 'edge-1', + source: 'node-1', + target: 'node-2', + type: 'graph-edge', + data: { label: '连接线' }, + }, + ]; + + return ( +
+ + + +
+ ); +}; + +export default App; +``` + +## 监听变化 + +编辑器支持通过回调函数监听节点和边的变化(打开控制台查看console面板): + +```jsx +import React, { useState } from 'react'; +import { GraphProvider, GraphCanvas, AddNode, ClearCanvas } from '@graphscope/studio-flow-editor'; +import { Card, Divider } from 'antd'; +import { Toolbar } from '@graphscope/studio-components'; + +const App = () => { + const [nodes, setNodes] = useState([]); + const [edges, setEdges] = useState([]); + + const handleNodesChange = newNodes => { + console.log('节点发生变化:', newNodes); + setNodes(newNodes); + }; + + const handleEdgesChange = newEdges => { + console.log('边发生变化:', newEdges); + setEdges(newEdges); + }; + + const handleSelectionChange = (selectedNodes, selectedEdges) => { + console.log('选中节点:', selectedNodes); + console.log('选中边:', selectedEdges); + setSelectedNodes(selectedNodes); + setSelectedEdges(selectedEdges); + }; + + return ( +
+ + + + + + +
+ +

节点变化:{JSON.stringify(nodes)}

+
+
+
+ +

{JSON.stringify(edges)}

+
+
+
+
+
+ ); +}; + +export default App; +``` + +## 添加自定义控制面板 + +可以利用内置的钩子函数在编辑器中添加自定义控制面板: + +```jsx +import React from 'react'; +import { + GraphProvider, + GraphCanvas, + useGraphStore, + useAddNode, + useClearCanvas, + useExportSvg, +} from '@graphscope/studio-flow-editor'; +import { MiniMap } from 'reactflow'; + +// 自定义控制面板组件 +const ControlPanel = () => { + const { store } = useGraphStore(); + const { nodes, edges, currentId, currentType } = store; + const { handleAddVertex } = useAddNode(); + const { handleClear } = useClearCanvas(); + const { exportSvg } = useExportSvg(); + return ( +
+
+ + + +
+
+
节点数量: {nodes.length}
+
边数量: {edges.length}
+ {currentId && ( +
+ 当前选中: {currentType === 'nodes' ? '节点' : '边'} {currentId} +
+ )} +
+
+ ); +}; + +const App = () => { + return ( +
+ + + + + + +
+ ); +}; + +export default App; +``` + +## 使用内置控制组件 + +Studio Flow Editor 提供了几个预定义的控制组件,可以直接使用: + +```jsx +import React from 'react'; +import { Toolbar } from '@graphscope/studio-components'; +import { GraphProvider, GraphCanvas, AddNode, ClearCanvas, ExportSvg } from '@graphscope/studio-flow-editor'; + +const App = () => { + return ( +
+ + + + + + + + + +
+ ); +}; + +export default App; +``` + + + +## 自定义节点编辑器示例 + +```jsx +import React from 'react'; +import { Toolbar } from '@graphscope/studio-components'; +import { + GraphProvider, + GraphCanvas, + AddNode, + ClearCanvas, + useGraphStore, +} from '@graphscope/studio-flow-editor'; + +// 自定义节点编辑面板 +const CustomNodePanel = () => { + const { store, updateStore } = useGraphStore(); + const { currentId, nodes, edges } = store; + + const currentItem = [...nodes,...edges].find(item => item.id === currentId); + + + const updateNodeLabel = newLabel => { + updateStore(draft => { + const node = draft.nodes.find(n => n.id === currentId); + const edge = draft.edges.find(e => e.id === currentId); + if (node) { + node.data.label = newLabel; + } + if(edge){ + edge.data.label = newLabel; + } + }); + }; + + return ( +
+ {currentItem && updateNodeLabel(e.target.value)} />} +
+ ); +}; + +// 完整示例 +const MyGraphCanvas = () => { + return ( +
+ + + + + + + + + +
+ ); +}; +export default MyGraphCanvas; +``` diff --git a/packages/studio-flow-editor/docs/api.md b/packages/studio-flow-editor/docs/api.md new file mode 100644 index 000000000..7a10ef834 --- /dev/null +++ b/packages/studio-flow-editor/docs/api.md @@ -0,0 +1,411 @@ +--- +order: 6 +title: API 文档 +--- + +# API 文档 + +本文档详细介绍了 `@graphscope/studio-flow-editor` 包提供的所有 API,包括组件、钩子和类型定义。 + +## GraphProvider + +### 描述 + +`GraphProvider` 是图编辑器的状态提供者,用于创建和管理图编辑器的状态。所有需要访问或修改图状态的组件都必须放在其内部。 + +### 属性 + +| 属性名 | 类型 | 必填 | 默认值 | 描述 | +| ---------- | --------- | ---- | -------------- | ---------------------------------- | +| `id` | string | 否 | 自动生成的UUID | 图实例ID,用于区分多个图编辑器实例 | +| `children` | ReactNode | 是 | - | 子组件 | + +### 示例 + +```bash + + + {/* 其他需要访问图状态的组件 */} + +``` + +## GraphCanvas + +### 描述 + +`GraphCanvas` 是主要的图编辑器组件,提供了交互式的图形编辑界面。它必须包裹在 `GraphProvider` 内部。 + +### 属性 + +| 属性名 | 类型 | 必填 | 默认值 | 描述 | +| ------------------- | ---------------------------------------------------- | ---- | ------ | ------------------------------------ | +| `children` | ReactNode | 否 | - | 子组件,可用于添加自定义UI | +| `nodesDraggable` | boolean | 否 | true | 节点是否可拖动 | +| `isPreview` | boolean | 否 | false | 是否预览模式,预览模式下禁用编辑功能 | +| `onNodesChange` | (nodes: ISchemaNode[]) => void | 否 | - | 节点变化时的回调函数 | +| `onEdgesChange` | (edges: ISchemaEdge[]) => void | 否 | - | 边变化时的回调函数 | +| `onSelectionChange` | (nodes: ISchemaNode[], edges: ISchemaEdge[]) => void | 否 | - | 选择变化时的回调函数 | +| `noDefaultLabel` | boolean | 否 | false | 是否不使用默认标签 | +| `defaultNodes` | ISchemaNode[] | 否 | [] | 默认节点数据 | +| `defaultEdges` | ISchemaEdge[] | 否 | [] | 默认边数据 | + +### 示例 + +```bash + console.log('Nodes changed:', nodes)} + onEdgesChange={(edges) => console.log('Edges changed:', edges)} + noDefaultLabel={false} + defaultNodes={initialNodes} + defaultEdges={initialEdges} +> + {/* 可选的自定义UI组件 */} + +``` + +## Hooks + +### useGraphStore + +#### 描述 + +访问和修改图编辑器状态的主要钩子。 + +#### 签名 + +```bash +function useGraphStore(id?: string): { + store: GraphState; + updateStore: (updater: (draft: GraphState) => void) => void; +}; +``` + +#### 参数 + +| 参数名 | 类型 | 必填 | 默认值 | 描述 | +| ------ | ------ | ---- | -------------- | ---------------- | +| `id` | string | 否 | 当前上下文的ID | 要访问的图实例ID | + +#### 返回值 + +| 名称 | 类型 | 描述 | +| ------------- | ---------------------------------------------- | ------------------ | +| `store` | GraphState | 图状态对象 | +| `updateStore` | (updater: (draft: GraphState) => void) => void | 用于更新状态的函数 | + +#### 示例 + +```bash +import { useGraphStore } from '@graphscope/studio-flow-editor'; + +const MyComponent = () => { + const { store, updateStore } = useGraphStore(); + + const addNewNode = () => { + updateStore(draft => { + draft.nodes.push({ + id: `node-${Date.now()}`, + position: { x: 100, y: 100 }, + data: { label: '新节点' }, + type: 'graph-node' + }); + }); + }; + + return ( +
+ +
节点数量: {store.nodes.length}
+
+ ); +}; +``` + +### useClearCanvas + +#### 描述 + +提供清除画布或删除选中元素的功能。 + +#### 签名 + +```bash +function useClearCanvas(): { + handleClear: () => void; +}; +``` + +#### 返回值 + +| 名称 | 类型 | 描述 | +| ------------- | ---------- | ---------------------------- | +| `handleClear` | () => void | 删除选中元素或清空画布的函数 | + +#### 行为 + +- 如果有选中的节点(selectedNodeIds 不为空),会删除这些节点及与之相关的边 +- 如果有选中的边(selectedEdgeIds 不为空),会删除这些边 +- 如果没有选中任何元素,会清空整个画布并重置标签索引 + +#### 示例 + +```bash +import { useClearCanvas } from '@graphscope/studio-flow-editor'; + +const DeleteButton = () => { + const { handleClear } = useClearCanvas(); + + return ( + + ); +}; +``` + +### useAddNode + +#### 描述 + +提供添加新节点的功能。 + +#### 签名 + +```bash +function useAddNode({noDefaultLabel}:{noDefaultLabel: string}): {handleAddVertex:(position?: { x: number; y: number }) => void;} +``` + +#### 返回值 + +| 名称 | 类型 | 描述 | +| ----------------- | --------------------------------------------- | -------------- | +| `handleAddVertex` | (position?: { x: number; y: number }) => void | 添加节点的函数 | + +#### 参数 + +| 参数名 | 类型 | 必填 | 默认值 | 描述 | +| ---------- | ------------------------ | ---- | -------- | ---------------- | +| `position` | { x: number; y: number } | 否 | 视口中心 | 新节点的位置坐标 | + +#### 示例 + +```bash +import { useAddNode } from '@graphscope/studio-flow-editor'; + +const AddNodeButton = () => { + const handleAddVertex = useAddNode(); + + return ; +}; +``` + +### useExportSvg + +#### 描述 + +提供导出SVG图片的功能。 + +#### 签名 + +```bash +function useExportSvg(): { exportSvg: ({name?: string,parentId?: string}) => void }; +``` + +#### 返回值 + +| 名称 | 类型 | 描述 | +| ----------- | ------------------------------------------- | ------------- | +| `exportSvg` | ({name?: string,parentId?: string}) => void | 导出SVG的函数 | + +#### 参数 + +| 参数名 | 类型 | 必填 | 默认值 | 描述 | +| ------ | ------ | ---- | ----------- | -------------- | +| `name` | string | 否 | 'graph.svg' | 导出文件的名称 | +| `parentId` | string | 否 | - | 当存在多个图形实例时,指定要导出的图形实例的上层dom ID | + +#### 示例 + +```bash +import { useExportSvg } from '@graphscope/studio-flow-editor'; + +const ExportButton = () => { + const { exportSvg } = useExportSvg(); + + return ; +}; +``` + +## 类型定义 + +### GraphState + +图编辑器的完整状态类型。 + +```typescript +interface GraphState { + displayMode: 'graph' | 'table'; // 显示模式 + nodes: ISchemaNode[]; // 节点列表 + edges: ISchemaEdge[]; // 边列表 + nodePositionChange: NodePositionChange[]; // 节点位置变更 + hasLayouted: boolean; // 是否已布局 + elementOptions: { + // 元素选项 + isEditable: boolean; // 是否可编辑 + isConnectable: boolean; // 是否可连接 + }; + theme: { + // 主题 + primaryColor: string; // 主色调 + }; + currentId: string; // 当前选中元素ID + currentType: 'nodes' | 'edges'; // 当前选中元素类型 + selectedNodeIds: string[]; // 选中的节点ID数组 + selectedEdgeIds: string[]; // 选中的边ID数组 +} +``` + +### ISchemaNode + +图节点的类型定义。 + +```typescript +type ISchemaNode = Node; + +interface INodeData { + label: string; // 节点标签 + disabled?: boolean; // 是否禁用 + properties?: Property[]; // 节点属性 + dataFields?: string[]; // 数据字段 + delimiter?: string; // 分隔符 + datatype?: 'csv' | 'odps'; // 数据类型 + filelocation?: string; // 文件位置 + [key: string]: any; // 其他自定义属性 +} +``` + +### ISchemaEdge + +图边的类型定义。 + +```typescript +type ISchemaEdge = Edge & { data: IEdgeData }; + +interface IEdgeData { + label: string; // 边标签 + disabled?: boolean; // 是否禁用 + saved?: boolean; // 是否已保存 + properties?: Property[]; // 边属性 + source_vertex_fields?: Property; // 源节点字段 + target_vertex_fields?: Property; // 目标节点字段 + dataFields?: string[]; // 数据字段 + delimiter?: string; // 分隔符 + datatype?: 'csv' | 'odps'; // 数据类型 + filelocation?: string; // 文件位置 + _extra?: { + // 额外配置 + type?: string; // 类型 + offset?: string; // 偏移量 + isLoop: boolean; // 是否自环 + isRevert?: boolean; // 是否反向 + isPoly?: boolean; // 是否多边 + index?: number; // 索引 + count?: number; // 计数 + }; + [key: string]: any; // 其他自定义属性 +} +``` + +## 工具函数 + +### getBBox + +计算一组节点的边界框。 + +#### 签名 + +```typescript +function getBBox(nodes: Node[]): { + x: number; + y: number; + width: number; + height: number; +}; +``` + +#### 参数 + +| 参数名 | 类型 | 描述 | +| ------- | ------ | ---------------------- | +| `nodes` | Node[] | 要计算边界框的节点数组 | + +#### 返回值 + +| 属性名 | 类型 | 描述 | +| -------- | ------ | ------------------- | +| `x` | number | 边界框左上角的X坐标 | +| `y` | number | 边界框左上角的Y坐标 | +| `width` | number | 边界框宽度 | +| `height` | number | 边界框高度 | + +### createNodeLabel + +生成节点的默认标签,格式为 "Vertex_n",其中 n 是自增序号。 + +#### 签名 + +```typescript +function createNodeLabel(): string; +``` + +#### 返回值 + +节点标签字符串,例如 "Vertex_1", "Vertex_2" 等。 + +### createEdgeLabel + +生成边的默认标签,格式为 "Edge_n",其中 n 是自增序号。 + +#### 签名 + +```typescript +function createEdgeLabel(): string; +``` + +#### 返回值 + +边标签字符串,例如 "Edge_1", "Edge_2" 等。 + +### resetIndex + +重置节点和边标签的自增序号,使之重新从1开始。 + +#### 签名 + +```typescript +function resetIndex(): void; +``` + +### fakeSnapshot + +创建对象的深拷贝。 + +#### 签名 + +```typescript +function fakeSnapshot(obj: T): T; +``` + +#### 参数 + +| 参数名 | 类型 | 描述 | +| ------ | ---- | -------------- | +| `obj` | T | 要深拷贝的对象 | + +#### 返回值 + +| 类型 | 描述 | +| ---- | -------------- | +| T | 原对象的深拷贝 | diff --git a/packages/studio-flow-editor/docs/components.md b/packages/studio-flow-editor/docs/components.md new file mode 100644 index 000000000..964bc9390 --- /dev/null +++ b/packages/studio-flow-editor/docs/components.md @@ -0,0 +1,218 @@ +--- +order: 4 +title: 组件文档 +--- +# 组件文档 + +`@graphscope/studio-flow-editor` 提供了几个预构建的组件,可以直接集成到您的应用中,简化图编辑器的操作与控制。 + +## AddNode + +### 描述 + +一个添加新节点的按钮组件。 + +### 属性 + +| 属性名 | 类型 | 必填 | 默认值 | 描述 | +|-------|------|------|-------|------| +| `style` | React.CSSProperties | 否 | - | 内联样式对象 | + +### 示例 + +```bash +import { AddNode } from '@graphscope/studio-flow-editor'; + +// 基本用法 + + +// 自定义样式和文本 + +``` + +## ClearCanvas + +### 描述 + +一个清除画布或删除选中元素的按钮组件。 + +### 属性 + +| 属性名 | 类型 | 必填 | 默认值 | 描述 | +|-------|------|------|-------|------| +| `style` | React.CSSProperties | 否 | - | 内联样式对象 | + +### 行为 + +- 如果有选中的节点,会删除这些节点及相关边 +- 如果有选中的边,会删除这些边 +- 如果没有选中任何元素,会清空整个画布 + +### 示例 + +```bash +import { ClearCanvas } from '@graphscope/studio-flow-editor'; + +// 基本用法 + + +// 自定义样式 + +``` + +## ExportSvg + +### 描述 + +一个导出图为SVG文件的按钮组件。 + +### 属性 + +| 属性名 | 类型 | 必填 | 默认值 | 描述 | +|-------|------|------|-------|------| +| `style` | React.CSSProperties | 否 | - | 内联样式对象 | +| `fileName` | string | 'graph.svg' | - | 导出文件名 | +| `parentId` | string | 否 | - | 当页面内存在多个图形实例是,可指定导出图上层dom的id | +### 示例 + +```bash +import { ExportSvg } from '@graphscope/studio-flow-editor'; + +// 基本用法 + + +// 自定义样式和文件名 + +``` + +## 组合使用 + +这些组件通常组合在一起使用,创建一个完整的工具栏: + +```jsx +import React from 'react'; +import { + GraphProvider, + GraphCanvas, + AddNode, + ClearCanvas, + ExportSvg +} from '@graphscope/studio-flow-editor'; + +const ToolbarStyle = { + position: 'absolute', + top: '10px', + right: '10px', + zIndex: 10, + display: 'flex', + gap: '8px', + background: 'white', + padding: '8px', + borderRadius: '4px', + boxShadow: '0 2px 6px rgba(0,0,0,0.1)' +}; + +const ButtonStyle = { + padding: '4px 12px', + borderRadius: '2px', + cursor: 'pointer', + border: '1px solid #d9d9d9', + backgroundColor: '#fff', +}; + +const App = () => { + return ( +
+ + +
+ + + +
+
+
+
+ ); +}; + +export default App; +``` + +## 自定义组件集成 + +您也可以将这些内置组件与自定义组件结合使用,创建更丰富的控制面板: + +```jsx +import React from 'react'; +import { + GraphProvider, + GraphCanvas, + useGraphStore, + AddNode, + ClearCanvas +} from '@graphscope/studio-flow-editor'; + +// 自定义信息面板 +const InfoPanel = () => { + const { store } = useGraphStore(); + + return ( +
+
节点数量: {store.nodes.length}
+
边数量: {store.edges.length}
+ {store.currentId && ( +
当前选中: {store.currentType === 'nodes' ? '节点' : '边'} {store.currentId}
+ )} +
+ ); +}; + +const App = () => { + return ( +
+ + +
+ + +
+ +
+
+
+ ); +}; + +export default App; +``` \ No newline at end of file diff --git a/packages/studio-flow-editor/docs/demos.md b/packages/studio-flow-editor/docs/demos.md new file mode 100644 index 000000000..3f0be00dc --- /dev/null +++ b/packages/studio-flow-editor/docs/demos.md @@ -0,0 +1,1144 @@ +--- +order: 8 +title: 进阶示例 +--- + +# 进阶示例 + +本文档提供了一些 `@graphscope/studio-flow-editor` 的进阶使用示例,帮助您深入了解如何利用这个库构建复杂的图编辑应用。 + +## 示例一:节点和边标签编辑器 + +这个示例展示了如何实现自定义的节点和边标签编辑功能。 + +```jsx +import React, { useState } from 'react'; +import { GraphProvider, GraphCanvas, useGraphStore, useAddNode, useClearCanvas } from '@graphscope/studio-flow-editor'; + +// 标签编辑面板组件 +const LabelEditor = () => { + const { store, updateStore } = useGraphStore(); + const { currentId, currentType, nodes, edges } = store; + const [labelValue, setLabelValue] = useState(''); + + // 当选中元素变化时,更新输入框的值 + React.useEffect(() => { + if (currentId && currentType) { + const currentItem = + currentType === 'nodes' ? nodes.find(item => item.id === currentId) : edges.find(item => item.id === currentId); + + if (currentItem && currentItem.data) { + setLabelValue(currentItem.data.label || ''); + } + } + }, [currentId, currentType, nodes, edges]); + + // 没有选中任何元素时不显示编辑器 + if (!currentId) { + return ( +
+ 请选择一个节点或边来编辑标签 +
+ ); + } + + // 处理标签变更 + const handleLabelChange = e => { + setLabelValue(e.target.value); + }; + + // 应用标签变更 + const applyLabelChange = () => { + updateStore(draft => { + if (currentType === 'nodes') { + const node = draft.nodes.find(n => n.id === currentId); + if (node) { + node.data.label = labelValue; + } + } else { + const edge = draft.edges.find(e => e.id === currentId); + if (edge) { + edge.data.label = labelValue; + } + } + }); + }; + + return ( +
+
编辑 {currentType === 'nodes' ? '节点' : '边'} 标签
+ + +
+ ); +}; + +// 工具栏组件 +const Toolbar = () => { + const { handleAddVertex } = useAddNode(); + const { handleClear } = useClearCanvas(); + + return ( +
+ + +
+ ); +}; + +// 应用组件 +const App = () => { + return ( +
+ + + + + + +
+ ); +}; + +export default App; +``` + +## 示例二:生成Cypher查询语句 + +这个示例展示了如何根据图编辑器中的节点和边生成Cypher查询语句。 + +```jsx +import React, { useState, useEffect } from 'react'; +import { MiniMap, Background } from 'reactflow'; +import { GraphProvider, GraphCanvas, useGraphStore, useAddNode, useClearCanvas } from '@graphscope/studio-flow-editor'; + +// Cypher生成器组件 +const CypherGenerator = () => { + const { store } = useGraphStore(); + const { nodes, edges } = store; + const [cypherQuery, setCypherQuery] = useState(''); + + // 当节点或边变化时,生成Cypher查询 + useEffect(() => { + if (nodes.length === 0) { + setCypherQuery('// 请添加节点'); + return; + } + + // 生成MATCH语句 + const matchClauses = nodes.map((node, index) => { + // 使用节点标签作为节点类型,默认为"Node" + const nodeLabel = node.data.label || 'Node'; + // 使用"n0", "n1"等作为变量名 + const variableName = `n${index}`; + + // 生成属性对象 + const properties = node.data.properties + ? Object.entries(node.data.properties) + .map(([key, value]) => `${key}: ${typeof value === 'string' ? `"${value}"` : value}`) + .join(', ') + : ''; + + const propertiesClause = properties ? `{${properties}}` : ''; + + // 返回完整的匹配模式 + return `MATCH (${variableName}:${nodeLabel.replace(/\s+/g, '_')}${propertiesClause})`; + }); + + // 生成关系语句(WHERE子句) + const whereClauses = edges + .map((edge, index) => { + // 找到源节点和目标节点的索引 + const sourceIndex = nodes.findIndex(node => node.id === edge.source); + const targetIndex = nodes.findIndex(node => node.id === edge.target); + + if (sourceIndex === -1 || targetIndex === -1) return null; + + // 使用边标签作为关系类型,默认为"RELATES_TO" + const relationshipType = (edge.data.label || 'RELATES_TO').replace(/\s+/g, '_'); + + // 返回关系条件 + return `MATCH (n${sourceIndex})-[:${relationshipType}]->(n${targetIndex})`; + }) + .filter(Boolean); // 过滤掉无效的关系 + + // 生成RETURN语句 + const returnClause = `RETURN ${nodes.map((_, index) => `n${index}`).join(', ')}`; + + // 组合完整查询 + const query = [...matchClauses, ...whereClauses, returnClause].join('\n'); + + setCypherQuery(query); + }, [nodes, edges]); + + // 复制查询到剪贴板 + const copyToClipboard = () => { + navigator.clipboard + .writeText(cypherQuery) + .then(() => { + alert('已复制到剪贴板'); + }) + .catch(err => { + console.error('复制失败:', err); + }); + }; + + return ( +
+
+ Cypher查询 + +
+
+        {cypherQuery}
+      
+
+ ); +}; + +// 工具栏组件 +const Toolbar = () => { + const { handleAddVertex } = useAddNode(); + const { handleClear } = useClearCanvas(); + const { store, updateStore } = useGraphStore(); + + // 添加示例图 + const addExampleGraph = () => { + // 先清空现有图 + updateStore(draft => { + draft.nodes = []; + draft.edges = []; + }); + + // 添加示例节点 + const personNode = { + id: 'node-1', + position: { x: 100, y: 100 }, + type: 'graph-node', + data: { + label: 'Person', + properties: { + name: 'Alice', + age: 30, + }, + }, + }; + + const companyNode = { + id: 'node-2', + position: { x: 300, y: 100 }, + type: 'graph-node', + data: { + label: 'Company', + properties: { + name: 'ACME Corp', + founded: 2010, + }, + }, + }; + + const productNode = { + id: 'node-3', + position: { x: 300, y: 250 }, + type: 'graph-node', + data: { + label: 'Product', + properties: { + name: 'Widget X', + price: 99.99, + }, + }, + }; + + // 添加示例边 + const worksAtEdge = { + id: 'edge-1', + source: 'node-1', + target: 'node-2', + type: 'graph-edge', + data: { label: 'WORKS_AT' }, + }; + + const createdEdge = { + id: 'edge-2', + source: 'node-2', + target: 'node-3', + type: 'graph-edge', + data: { label: 'CREATED' }, + }; + + const purchasedEdge = { + id: 'edge-3', + source: 'node-1', + target: 'node-3', + type: 'graph-edge', + data: { label: 'PURCHASED' }, + }; + + // 更新图 + updateStore(draft => { + draft.nodes = [personNode, companyNode, productNode]; + draft.edges = [worksAtEdge, createdEdge, purchasedEdge]; + }); + }; + + return ( +
+ + + +
+ ); +}; + +// 节点数据面板 +const NodeDataPanel = () => { + const { store, updateStore } = useGraphStore(); + const { currentId, currentType, nodes } = store; + const [properties, setProperties] = useState({}); + const [newKey, setNewKey] = useState(''); + const [newValue, setNewValue] = useState(''); + + // 当选中节点变化时,更新属性面板 + useEffect(() => { + if (currentId && currentType === 'nodes') { + const currentNode = nodes.find(node => node.id === currentId); + if (currentNode && currentNode.data) { + setProperties(currentNode.data.properties || {}); + } else { + setProperties({}); + } + } else { + setProperties({}); + } + }, [currentId, currentType, nodes]); + + // 没有选中节点时不显示面板 + if (!currentId || currentType !== 'nodes') { + return null; + } + + // 添加新属性 + const addProperty = () => { + if (!newKey.trim()) return; + + // 尝试转换为数字 + let parsedValue = newValue; + if (!isNaN(Number(newValue))) { + parsedValue = Number(newValue); + } + + const updatedProperties = { + ...properties, + [newKey]: parsedValue, + }; + + setProperties(updatedProperties); + updateStore(draft => { + const node = draft.nodes.find(n => n.id === currentId); + if (node) { + node.data.properties = updatedProperties; + } + }); + + // 清空输入框 + setNewKey(''); + setNewValue(''); + }; + + // 删除属性 + const removeProperty = key => { + const { [key]: _, ...rest } = properties; + setProperties(rest); + + updateStore(draft => { + const node = draft.nodes.find(n => n.id === currentId); + if (node) { + node.data.properties = rest; + } + }); + }; + + return ( +
+
节点属性编辑
+ + {/* 现有属性 */} +
+ {Object.entries(properties).length === 0 ? ( +
暂无属性
+ ) : ( + Object.entries(properties).map(([key, value]) => ( +
+
+ {key}: {String(value)} +
+ +
+ )) + )} +
+ + {/* 添加新属性 */} +
+
+ setNewKey(e.target.value)} + placeholder="属性名" + style={{ + padding: '6px 8px', + border: '1px solid #d9d9d9', + borderRadius: '2px', + flex: 1, + }} + /> + setNewValue(e.target.value)} + placeholder="属性值" + style={{ + padding: '6px 8px', + border: '1px solid #d9d9d9', + borderRadius: '2px', + flex: 1, + }} + /> +
+ +
+
+ ); +}; + +// 应用组件 +const App = () => { + return ( +
+ + + + + + + + + +
+ ); +}; + +export default App; +``` + +## 示例三:结合多个功能的完整应用 + +以下是一个结合了多项功能的完整应用示例,包括: + +- 节点和边标签编辑 +- 节点属性编辑 +- Cypher查询生成 +- 图数据导入/导出 +- 自定义样式 + +```jsx +import React, { useState, useEffect } from 'react'; +import { + GraphProvider, + GraphCanvas, + useGraphStore, + useAddNode, + useClearCanvas, + useExportSvg, +} from '@graphscope/studio-flow-editor'; + +// 主应用组件 +const GraphApplication = () => { + return ( +
+ + + + + + + + +
+ ); +}; + +// 主工具栏 +const MainToolbar = () => { + const {handleAddVertex} = useAddNode(); + const { handleClear } = useClearCanvas(); + const {exportSvg} = useExportSvg(); + const { store, updateStore } = useGraphStore(); + + // 导出图数据为JSON + const exportJSON = () => { + const { nodes, edges } = store; + const dataStr = JSON.stringify({ nodes, edges }, null, 2); + const dataUri = `data:application/json;charset=utf-8,${encodeURIComponent(dataStr)}`; + + const exportLink = document.createElement('a'); + exportLink.setAttribute('href', dataUri); + exportLink.setAttribute('download', 'graph-data.json'); + document.body.appendChild(exportLink); + exportLink.click(); + document.body.removeChild(exportLink); + }; + + // 导入JSON数据 + const importJSON = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + + input.onchange = e => { + const file = e.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = event => { + try { + const { nodes, edges } = JSON.parse(event.target.result); + updateStore(draft => { + draft.nodes = nodes; + draft.edges = edges; + }); + } catch (error) { + console.error('导入失败:', error); + alert('导入失败: ' + error.message); + } + }; + reader.readAsText(file); + }; + + input.click(); + }; + + return ( +
+ + + + + +
+ ); +}; + +// 属性面板 +const PropertiesPanel = () => { + const { store, updateStore } = useGraphStore(); + const { currentId, currentType, nodes, edges } = store; + const [labelValue, setLabelValue] = useState(''); + const [properties, setProperties] = useState({}); + const [newKey, setNewKey] = useState(''); + const [newValue, setNewValue] = useState(''); + + // 当选中元素变化时,更新面板 + useEffect(() => { + if (currentId && currentType) { + const currentItem = + currentType === 'nodes' ? nodes.find(item => item.id === currentId) : edges.find(item => item.id === currentId); + + if (currentItem && currentItem.data) { + setLabelValue(currentItem.data.label || ''); + setProperties(currentItem.data.properties || {}); + } + } else { + setLabelValue(''); + setProperties({}); + } + }, [currentId, currentType, nodes, edges]); + + if (!currentId) return null; + + // 更新标签 + const updateLabel = () => { + updateStore(draft => { + if (currentType === 'nodes') { + const node = draft.nodes.find(n => n.id === currentId); + if (node) { + node.data.label = labelValue; + } + } else { + const edge = draft.edges.find(e => e.id === currentId); + if (edge) { + edge.data.label = labelValue; + } + } + }); + }; + + // 添加属性 + const addProperty = () => { + if (!newKey.trim()) return; + + // 尝试转换为数字 + let parsedValue = newValue; + if (!isNaN(Number(newValue))) { + parsedValue = Number(newValue); + } + + const updatedProperties = { + ...properties, + [newKey]: parsedValue, + }; + + setProperties(updatedProperties); + updateStore(draft => { + if (currentType === 'nodes') { + const node = draft.nodes.find(n => n.id === currentId); + if (node) { + node.data.properties = updatedProperties; + } + } else { + const edge = draft.edges.find(e => e.id === currentId); + if (edge) { + edge.data.properties = updatedProperties; + } + } + }); + + setNewKey(''); + setNewValue(''); + }; + + // 删除属性 + const removeProperty = key => { + const { [key]: _, ...rest } = properties; + setProperties(rest); + + updateStore(draft => { + if (currentType === 'nodes') { + const node = draft.nodes.find(n => n.id === currentId); + if (node) { + node.data.properties = rest; + } + } else { + const edge = draft.edges.find(e => e.id === currentId); + if (edge) { + edge.data.properties = rest; + } + } + }); + }; + + return ( +
+
+ {currentType === 'nodes' ? '节点属性' : '边属性'} +
+ + {/* 标签编辑 */} +
+
标签:
+
+ setLabelValue(e.target.value)} + style={{ + padding: '6px 8px', + border: '1px solid #d9d9d9', + borderRadius: '2px', + flex: 1, + }} + /> + +
+
+ + {/* 属性列表 */} +
+
属性:
+ {Object.entries(properties).length === 0 ? ( +
暂无属性
+ ) : ( +
+ {Object.entries(properties).map(([key, value]) => ( +
+
+ {key}: {String(value)} +
+ +
+ ))} +
+ )} + + {/* 添加新属性 */} +
+
+ setNewKey(e.target.value)} + placeholder="属性名" + style={{ + padding: '6px 8px', + border: '1px solid #d9d9d9', + borderRadius: '2px', + flex: 1, + }} + /> + setNewValue(e.target.value)} + placeholder="属性值" + style={{ + padding: '6px 8px', + border: '1px solid #d9d9d9', + borderRadius: '2px', + flex: 1, + }} + /> +
+ +
+
+
+ ); +}; + +// Cypher查询面板 +const CypherPanel = () => { + const { store } = useGraphStore(); + const { nodes, edges } = store; + const [cypherQuery, setCypherQuery] = useState(''); + + // 当节点或边变化时,生成Cypher查询 + useEffect(() => { + if (nodes.length === 0) { + setCypherQuery('// 请添加节点'); + return; + } + + // 生成MATCH语句 + const matchClauses = nodes.map((node, index) => { + const nodeLabel = node.data.label || 'Node'; + const variableName = `n${index}`; + + // 生成属性对象 + let propertiesClause = ''; + if (node.data.properties && Object.keys(node.data.properties).length > 0) { + const properties = Object.entries(node.data.properties) + .map(([key, value]) => `${key}: ${typeof value === 'string' ? `"${value}"` : value}`) + .join(', '); + propertiesClause = `{${properties}}`; + } + + return `MATCH (${variableName}:${nodeLabel.replace(/\s+/g, '_')}${propertiesClause})`; + }); + + // 生成关系语句 + const whereClauses = edges + .map((edge, index) => { + const sourceIndex = nodes.findIndex(node => node.id === edge.source); + const targetIndex = nodes.findIndex(node => node.id === edge.target); + + if (sourceIndex === -1 || targetIndex === -1) return null; + + const relationshipType = (edge.data.label || 'RELATES_TO').replace(/\s+/g, '_'); + + // 生成关系属性 + let propertiesClause = ''; + if (edge.data.properties && Object.keys(edge.data.properties).length > 0) { + const properties = Object.entries(edge.data.properties) + .map(([key, value]) => `${key}: ${typeof value === 'string' ? `"${value}"` : value}`) + .join(', '); + propertiesClause = `{${properties}}`; + } + + return `MATCH (n${sourceIndex})-[:${relationshipType}${propertiesClause}]->(n${targetIndex})`; + }) + .filter(Boolean); + + // 生成RETURN语句 + const returnClause = `RETURN ${nodes.map((_, index) => `n${index}`).join(', ')}`; + + // 组合完整查询 + const query = [...matchClauses, ...whereClauses, returnClause].join('\n'); + + setCypherQuery(query); + }, [nodes, edges]); + + // 复制查询到剪贴板 + const copyToClipboard = () => { + navigator.clipboard + .writeText(cypherQuery) + .then(() => { + alert('已复制到剪贴板'); + }) + .catch(err => { + console.error('复制失败:', err); + }); + }; + + return ( +
+
+ Cypher查询 + +
+
+        {cypherQuery}
+      
+
+ ); +}; + +// 状态栏 +const StatusBar = () => { + const { store } = useGraphStore(); + const { nodes, edges, currentId, currentType } = store; + + return ( +
+
+ 节点: {nodes.length} | 边: {edges.length} +
+
{currentId && `当前选中: ${currentType === 'nodes' ? '节点' : '边'} ${currentId}`}
+
+ ); +}; + +// 共用样式 +const buttonStyle = { + padding: '6px 12px', + background: '#1890ff', + color: 'white', + border: 'none', + borderRadius: '2px', + cursor: 'pointer', +}; + +export default GraphApplication; +``` diff --git a/packages/studio-flow-editor/docs/hooks.md b/packages/studio-flow-editor/docs/hooks.md new file mode 100644 index 000000000..f09c7a956 --- /dev/null +++ b/packages/studio-flow-editor/docs/hooks.md @@ -0,0 +1,153 @@ +--- +order: 5 +title: Hooks +--- + +# 钩子函数 + +`@graphscope/studio-flow-editor` 包提供了多个自定义钩子(Hooks),帮助您与图编辑器交互并操作其状态。 + +## 核心钩子 + +### useGraphStore + +用于访问和更新图状态的主要钩子。 + +```bash +import { useGraphStore } from '@graphscope/studio-flow-editor'; + +const MyComponent = () => { + const { store, updateStore } = useGraphStore(); + const { nodes, edges, currentId, currentType } = store; + + const handleAddNode = () => { + updateStore(draft => { + draft.nodes.push({ + id: 'node-' + Date.now(), + position: { x: 100, y: 100 }, + data: { label: '新节点' }, + type: 'graph-node' + }); + }); + }; + + return ( + + ); +}; +``` + +#### 存储属性 + +| 属性 | 类型 | 说明 | +| -------------------- | ------------------------------------------------- | ---------------------- | +| `displayMode` | `'graph' \| 'table'` | 当前显示模式 | +| `nodes` | `ISchemaNode[]` | 图节点数组 | +| `edges` | `ISchemaEdge[]` | 图边数组 | +| `nodePositionChange` | `NodePositionChange[]` | 节点位置变更记录 | +| `hasLayouted` | `boolean` | 是否已应用自动布局 | +| `elementOptions` | `{ isEditable: boolean, isConnectable: boolean }` | 元素交互选项 | +| `theme` | `{ primaryColor: string }` | 主题设置 | +| `currentId` | `string` | 当前选中的节点或边的ID | +| `currentType` | `'nodes' \| 'edges'` | 当前选中元素的类型 | + +### useClearCanvas + +提供当存在选中节点或连线时,删除选中节点及相关连线与选中连线的功能,当不存在选中节点时,提供清空画布(删除所有节点和边)的功能。 + +```bash +import { useClearCanvas } from '@graphscope/studio-flow-editor'; + +const ClearButton = () => { + const {handleClear} = useClearCanvas(); + + return ( + + ); +}; +``` + +### useAddNode + +提供添加新节点到画布的功能。 + +```bash +import { useAddNode } from '@graphscope/studio-flow-editor'; + +const AddNodeButton = () => { + const { handleAddVertex } = useAddNode(); + + return ( + + ); +}; +``` + +### useExportSvg + +提供将图导出为SVG或图片的功能。 + +```bash +import { useExportSvg } from '@graphscope/studio-flow-editor'; + +const ExportButton = () => { + const {exportSvg} = useExportSvg(); + + return ( + + ); +}; +``` + +## 高级用法 + +### 组合多个钩子 + +```bash +import { useGraphStore, useAddNode, useClearCanvas } from '@graphscope/studio-flow-editor'; + +const GraphControls = () => { + const { store } = useGraphStore(); + const {handleAddVertex} = useAddNode(); + const {handleClear} = useClearCanvas(); + + return ( +
+ + +
节点数量: {store.nodes.length}
+
边数量: {store.edges.length}
+
+ ); +}; +``` + +### 自定义钩子管理节点选择 + +```bash +import { useGraphStore } from '@graphscope/studio-flow-editor'; +import { useCallback } from 'react'; + +export const useNodeSelection = () => { + const { store, updateStore } = useGraphStore(); + + const selectNode = useCallback((nodeId) => { + updateStore(draft => { + draft.currentId = nodeId; + draft.currentType = 'nodes'; + }); + }, [updateStore]); + + const isSelected = useCallback((nodeId) => { + return store.currentId === nodeId && store.currentType === 'nodes'; + }, [store.currentId, store.currentType]); + + return { selectNode, isSelected, selectedId: store.currentId }; +}; +``` diff --git a/packages/studio-flow-editor/docs/index.md b/packages/studio-flow-editor/docs/index.md new file mode 100644 index 000000000..737373f9b --- /dev/null +++ b/packages/studio-flow-editor/docs/index.md @@ -0,0 +1,103 @@ +--- +order: 0 +title: 写在前面 +--- + +# Studio Flow Editor + +## 写在前面 + +### 概述 + +`@graphscope/studio-flow-editor` 是一个基于 React 的图编辑器组件库,提供了一套完整的图数据可视化和交互编辑解决方案。该库基于 ReactFlow 构建,支持节点与边的直观编辑、自动布局以及高度可定制的图形表示。 + +### 主要特性 + +- **交互式编辑**:支持拖拽式创建、移动节点和连线 +- **状态管理**:集成 Zustand 进行高效状态管理 +- **多实例支持**:允许在同一页面创建多个独立图编辑器 +- **自动布局**:内置力导向图布局算法 +- **类型安全**:完整的 TypeScript 类型定义 +- **自定义外观**:可定制节点和边的样式与行为 +- **导出功能**:支持导出为 SVG 图片 + +### 主要组件一览 + +#### GraphCanvas + +主编辑器组件,提供可视化图编辑功能。 + +```typescript +interface ImportorProps { + id?: string; // 编辑器实例ID + children?: React.ReactNode; // 子组件 + nodesDraggable?: boolean; // 节点是否可拖拽 + isPreview?: boolean; // 是否为预览模式 + onNodesChange?: (nodes: ISchemaNode[]) => void; // 节点变化回调 + onEdgesChange?: (edges: ISchemaEdge[]) => void; // 边变化回调 + onSelectionChange?: (nodes: ISchemaNode[], edges: ISchemaEdge[]) => void; // 选择变化回调 + noDefaultLabel?: boolean; // 是否禁用默认标签 + defaultNodes?: ISchemaNode[]; // 初始节点 + defaultEdges?: ISchemaEdge[]; // 初始边 +} +``` + +#### GraphProvider + +状态管理提供者,负责管理图编辑器的所有状态。 + +```typescript +interface GraphProviderProps { + id?: string; // 图实例ID,默认自动生成 + children: React.ReactNode; // 子组件 +} +``` + +#### 状态结构 + +```typescript +interface GraphState { + displayMode: 'graph' | 'table'; // 显示模式 + nodes: ISchemaNode[]; // 节点列表 + edges: ISchemaEdge[]; // 边列表 + nodePositionChange: NodePositionChange[]; // 节点位置变更 + hasLayouted: boolean; // 是否已布局 + elementOptions: { // 元素选项 + isEditable: boolean; // 是否可编辑 + isConnectable: boolean; // 是否可连接 + }; + theme: { // 主题 + primaryColor: string; // 主色调 + }; + currentId: string; // 当前选中元素ID + currentType: 'nodes' | 'edges'; // 当前选中元素类型 + selectedNodeIds: string[]; // 选中的节点ID数组 + selectedEdgeIds: string[]; // 选中的边ID数组 +} +``` + +### 钩子(Hooks)一览 + +| 钩子名称 | 返回值 | 描述 | +|---------|-------|------| +| `useGraphStore` | `{ store, updateStore }` | 访问和更新图状态 | +| `useClearCanvas` | `{ handleClear }` | 清除画布或删除选中元素 | +| `useAddNode` | `{ handleAddVertex }` | 添加新节点到画布 | +| `useExportSvg` | `{ exportSvg }` | 导出图为SVG或图片 | + +### 工具组件一览 + +| 组件名称 | 描述 | +|---------|------| +| `AddNode` | 添加节点按钮组件 | +| `ClearCanvas` | 清空/删除选中元素按钮组件 | +| `ExportSvg` | 导出SVG按钮组件 | + +## 文档导航 + +- [快速开始](/floweditors/quick-start) - 安装指南和基本使用方法 +- [GraphProvider](/floweditors/provider) - 状态管理与Provider +- [GraphCanvas](/floweditors/graph-canvas) - GraphCanvas组件 +- [API文档](/floweditors/api) - 详细的API参考 +- [组件](/floweditors/components) - 工具组件使用指南 +- [示例](/floweditors/demos) - 实用示例和高级用法 diff --git a/packages/studio-flow-editor/docs/provider.md b/packages/studio-flow-editor/docs/provider.md new file mode 100644 index 000000000..62b531cf0 --- /dev/null +++ b/packages/studio-flow-editor/docs/provider.md @@ -0,0 +1,180 @@ +--- +order: 2 +title: GraphProvider +--- + +# 状态管理与Provider + +`@graphscope/studio-flow-editor` 包使用Provider组件来管理应用中的状态和上下文。这些Provider组件是图形编辑器正常运行的基础。 + +## GraphProvider + +`GraphProvider` 是图形编辑器的主要状态提供者。它创建一个上下文,用于保存和管理节点、边和其他图形相关数据的状态。 + +### 导入方式 + +```bash +import { GraphProvider } from '@graphscope/studio-flow-editor'; +``` + +### 基本用法 + +```bash +import React from 'react'; +import { GraphProvider, GraphCanvas } from '@graphscope/studio-flow-editor'; + +const App = () => { + return ( + + + {/* 其他需要访问图形状态的组件 */} + + ); +}; +``` + +### 属性配置 + +| 属性名 | 类型 | 默认值 | 说明 | +| ---------- | --------- | -------------- | ------------------------ | +| `id` | string | 自动生成的UUID | 该图实例的唯一标识符 | +| `children` | ReactNode | - | 需要访问图形状态的子组件 | + +### 状态管理 + +`GraphProvider` 内部使用 [Zustand](https://github.com/pmndrs/zustand)(通过 `@graphscope/use-zustand`)来管理状态。初始状态结构如下: + +```typescript +interface GraphState { + displayMode: 'graph' | 'table'; + nodes: ISchemaNode[]; + edges: ISchemaEdge[]; + nodePositionChange: NodePositionChange[]; + hasLayouted: boolean; + elementOptions: { + isEditable: boolean; + isConnectable: boolean; + }; + theme: { + primaryColor: string; + }; + currentId: string; + currentType: 'nodes' | 'edges'; + selectedNodeIds: string[]; + selectedEdgeIds: string[]; +} +``` + +## 多实例支持 + +可以在同一页面创建多个独立的图编辑器实例: + +```jsx +import React from 'react'; +import { Toolbar } from '@graphscope/studio-components'; +import { GraphProvider, GraphCanvas, AddNode, ClearCanvas, ExportSvg } from '@graphscope/studio-flow-editor'; +import { Divider } from 'antd'; + +const App = () => { + return ( +
+
+ + + + + + + + + +
+ +
+ + + + + + + + + +
+
+ ); +}; + +export default App; +``` + + +## ReactFlowProvider + +`ReactFlowProvider` 在内部用于为所有组件提供 ReactFlow 上下文。它已自动包含在 `GraphCanvas` 组件中,因此通常不需要直接使用它。 + +### 访问状态 + +要在 `GraphProvider` 的子组件中访问图状态,使用 `useGraphStore` 钩子: + +```bash +import { useGraphStore } from '@graphscope/studio-flow-editor'; + +const MyComponent = () => { + const { store, updateStore } = useGraphStore(); + + return ( +
+

节点数量: {store.nodes.length}

+

边数量: {store.edges.length}

+
+ ); +}; +``` + +### 更新状态 + +要更新图状态,使用 `useGraphStore` 提供的 `updateStore` 函数: + +```bash +import { useGraphStore } from '@graphscope/studio-flow-editor'; + +const AddRandomNode = () => { + const { updateStore } = useGraphStore(); + + const handleClick = () => { + updateStore(draft => { + draft.nodes.push({ + id: `node-${Math.random().toString(36).substring(2, 9)}`, + position: { x: Math.random() * 500, y: Math.random() * 500 }, + data: { label: '随机节点' }, + type: 'graph-node', + }); + }); + }; + + return ; +}; +``` + +## 高级用法 + +### 嵌套Provider + +可以嵌套Provider来创建复杂的状态层次结构: + +```bash +import { GraphProvider } from '@graphscope/studio-flow-editor'; +import { ThemeProvider } from 'your-theme-provider'; + +const App = () => { + return ( + + + + + + ); +}; +``` + diff --git a/packages/studio-flow-editor/docs/quick-start.md b/packages/studio-flow-editor/docs/quick-start.md new file mode 100644 index 000000000..ae7a2dd2c --- /dev/null +++ b/packages/studio-flow-editor/docs/quick-start.md @@ -0,0 +1,39 @@ +--- +order: 1 +title: 快速开始 +--- +# 快速开始 + +## 安装 + +```bash +# 使用npm +npm install @graphscope/studio-flow-editor + +# 使用yarn +yarn add @graphscope/studio-flow-editor + +# 使用pnpm +pnpm add @graphscope/studio-flow-editor +``` + +## 基本用法 + +下面是一个最简单的例子,展示了如何创建一个基本的图编辑器: + +```jsx +import React from 'react'; +import { GraphProvider, GraphCanvas } from '@graphscope/studio-flow-editor'; + +const App = () => { + return ( +
+ + + +
+ ); +}; + +export default App; +``` diff --git a/packages/studio-flow-editor/docs/utils.md b/packages/studio-flow-editor/docs/utils.md new file mode 100644 index 000000000..f5d0d9b85 --- /dev/null +++ b/packages/studio-flow-editor/docs/utils.md @@ -0,0 +1,157 @@ +--- +order: 7 +title: 工具函数 +--- +# 工具函数 + +`@graphscope/studio-flow-editor` 包提供了多个工具函数,帮助您处理图编辑中常见的任务。 + +## 布局工具 + +### getBBox + +计算一组节点的边界框,用于确定需要可见的区域。 + +```typescript +function getBBox(nodes: Node[]): { + x: number; + y: number; + width: number; + height: number; +}; +``` + +**示例:** +```bash +import { getBBox } from '@graphscope/studio-flow-editor'; + +const nodes = [ + { id: '1', position: { x: 100, y: 100 } }, + { id: '2', position: { x: 200, y: 300 } } +]; + +const bbox = getBBox(nodes); +// 返回: { x: 100, y: 100, width: 200, height: 300 } +``` + +## 标签工具 + +### createNodeLabel + +为新节点生成唯一标签。 + +```typescript +function createNodeLabel(): string; +``` + +**示例:** +```bash +import { createNodeLabel } from '@graphscope/studio-flow-editor'; + +const newNodeLabel = createNodeLabel(); +// 返回: "Vertex_1", "Vertex_2", 等 +``` + +### createEdgeLabel + +为新边生成唯一标签。 + +```bash +function createEdgeLabel(): string; +``` + +**示例:** +```bash +import { createEdgeLabel } from '@graphscope/studio-flow-editor'; + +const newEdgeLabel = createEdgeLabel(); +// 返回: "Edge_1", "Edge_2", 等 +``` + +### resetIndex + +重置用于生成节点和边标签的内部计数器。 + +```bash +function resetIndex(): void; +``` + +**示例:** +```bash +import { resetIndex } from '@graphscope/studio-flow-editor'; + +resetIndex(); +// 下次调用 createNodeLabel() 将返回 "Vertex_1" +``` + +## 数据处理工具 + +### fakeSnapshot + +创建对象的深拷贝。当您需要克隆图数据以避免意外修改时,这非常有用。 + +```bash +function fakeSnapshot(obj: T): T; +``` + +**示例:** +```bash +import { fakeSnapshot } from '@graphscope/studio-flow-editor'; + +const originalNodes = [...]; +const nodesCopy = fakeSnapshot(originalNodes); +// 现在可以修改 nodesCopy 而不会影响 originalNodes +``` + +## 类型定义 + +该包还导出了几个在处理图数据时有用的 TypeScript 类型定义: + +### INodeData + +```typescript +interface INodeData { + label: string; + disabled?: boolean; + properties?: Property[]; + dataFields?: string[]; + delimiter?: string; + datatype?: 'csv' | 'odps'; + filelocation?: string; + [key: string]: any; +} +``` + +### IEdgeData + +```typescript +interface IEdgeData { + label: string; + disabled?: boolean; + saved?: boolean; + properties?: Property[]; + source_vertex_fields?: Property; + target_vertex_fields?: Property; + dataFields?: string[]; + delimiter?: string; + datatype?: 'csv' | 'odps'; + filelocation?: string; + _extra?: { + type?: string; + offset?: string; + isLoop: boolean; + isRevert?: boolean; + isPoly?: boolean; + index?: number; + count?: number; + }; + [key: string]: any; +} +``` + +### ISchemaNode 和 ISchemaEdge + +```typescript +type ISchemaNode = Node; +type ISchemaEdge = Edge & { data: IEdgeData }; +``` diff --git a/packages/studio-flow-editor/index.html b/packages/studio-flow-editor/index.html new file mode 100644 index 000000000..c96f69e74 --- /dev/null +++ b/packages/studio-flow-editor/index.html @@ -0,0 +1,18 @@ + + + + + + Studio Graph Editor + + + +
+ + + diff --git a/packages/studio-flow-editor/index.tsx b/packages/studio-flow-editor/index.tsx new file mode 100644 index 000000000..e786751d2 --- /dev/null +++ b/packages/studio-flow-editor/index.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { GraphProvider, GraphCanvas, useGraphStore, useAddNode } from './src/index'; +import { createRoot } from 'react-dom/client'; +import { Button } from 'antd'; + +interface IAppProps {} +const Edit = () => { + const { store } = useGraphStore(); + const { handleAddVertex } = useAddNode(); + const { nodes } = store; + const printData = () => { + console.log('nodes::: ', nodes); + }; + return ( + <> + + + + ); +}; +const DrawGraph: React.FunctionComponent = props => { + return ( + <> +
+ + + + + +
+ + ); +}; + +createRoot(document.getElementById('root')!).render(); diff --git a/packages/studio-flow-editor/package.json b/packages/studio-flow-editor/package.json new file mode 100644 index 000000000..53e79400b --- /dev/null +++ b/packages/studio-flow-editor/package.json @@ -0,0 +1,55 @@ +{ + "name": "@graphscope/studio-flow-editor", + "version": "0.1.13", + "description": "", + "main": "lib/index.js", + "module": "es/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/GraphScope/portal.git" + }, + "files": [ + "es", + "lib", + "dist" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "scripts": { + "start": "father dev", + "build": "father build", + "start:site": "vite dev", + "build:site": "vite build && tsc" + }, + "peerDependencies": { + "react": "18.2.0", + "react-dom": "18.2.0" + }, + "dependencies": { + "@graphscope/studio-components": "workspace:*", + "@graphscope/use-zustand": "workspace:*", + "antd": "^5.22.2", + "d3-force": "latest", + "dagre": "latest", + "html-to-image": "^1.11.11", + "immer": "^10.1.1", + "lodash": "^4.17.21", + "react-intl": "^6.6.1", + "reactflow": "latest", + "rxjs": "^7.8.1", + "uuid": "^9.0.1", + "valtio": "2.0.0-rc.1", + "zustand": "^4.5.5" + }, + "devDependencies": { + "@types/d3-force": "latest", + "@types/lodash": "^4.14.202", + "@vitejs/plugin-react": "^4.2.1", + "vite": "^5.4.16" + }, + "author": "", + "license": "ISC" +} diff --git a/packages/studio-flow-editor/src/app/button-controller/add-node.tsx b/packages/studio-flow-editor/src/app/button-controller/add-node.tsx new file mode 100644 index 000000000..67625d9ce --- /dev/null +++ b/packages/studio-flow-editor/src/app/button-controller/add-node.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { Button, Tooltip } from 'antd'; +import { Icons } from '@graphscope/studio-components'; +import { FormattedMessage } from 'react-intl'; +import { useGraphStore } from '../store'; +import { useAddNode } from '../hooks/useAddNode'; + +const AddNodeIcon = Icons.AddNode; + +interface IAddNodeProps { + style?: React.CSSProperties; + noDefaultLabel?: boolean; +} +let addNodeIndex = 0; + +const AddNode: React.FunctionComponent = props => { + const { style, noDefaultLabel } = props; + const { store } = useGraphStore(); + const { elementOptions } = store; + const disabled = !elementOptions.isConnectable; + const tooltipText = disabled ? ( + + ) : ( + + ); + const { handleAddVertex } = useAddNode({ noDefaultLabel }); + + return ( + + + + ); +}; + +export default AddNode; diff --git a/packages/studio-importor/src/app/button-controller/clear-canvas.tsx b/packages/studio-flow-editor/src/app/button-controller/clear-canvas.tsx similarity index 63% rename from packages/studio-importor/src/app/button-controller/clear-canvas.tsx rename to packages/studio-flow-editor/src/app/button-controller/clear-canvas.tsx index 8c90fd8f5..d0559a40f 100644 --- a/packages/studio-importor/src/app/button-controller/clear-canvas.tsx +++ b/packages/studio-flow-editor/src/app/button-controller/clear-canvas.tsx @@ -1,44 +1,36 @@ import * as React from 'react'; import { Button, Tooltip } from 'antd'; - +import { useGraphStore } from '../store'; import { Icons, useStudioProvier } from '@graphscope/studio-components'; -import { resetIndex } from '../utils'; import { FormattedMessage } from 'react-intl'; +import { useClearCanvas } from '../hooks/useClearCanvas'; interface IAddNodeProps { style?: React.CSSProperties; } -import { useContext } from '@graphscope/use-zustand'; const ClearCanvas: React.FunctionComponent = props => { const { style } = props; - const { updateStore, store } = useContext(); + const { store } = useGraphStore(); const { elementOptions } = store; - const { isLight } = useStudioProvier(); + const { algorithm } = useStudioProvier(); + const isLight = algorithm === 'defaultAlgorithm'; /** svg pathFill */ let pathFill = () => { if (!isLight) { - return elementOptions.isEditable ? '#585858' : '#fff'; + return elementOptions.isEditable ? '#fff' : '#585858'; } else { - return elementOptions.isEditable ? '#ddd' : '#000'; + return elementOptions.isEditable ? '#000' : '#ddd'; } }; - const tooltipText = elementOptions.isEditable ? ( + const tooltipText = !elementOptions.isEditable ? ( ) : ( ); - - const handleClear = () => { - resetIndex(); - updateStore(draft => { - draft.nodes = []; - draft.edges = []; - }); - }; - + const { handleClear } = useClearCanvas(); return ( - - ); -}; - -export default AddNode; diff --git a/packages/studio-importor/src/app/button-controller/export-image.tsx b/packages/studio-importor/src/app/button-controller/export-image.tsx deleted file mode 100644 index e71d490ca..000000000 --- a/packages/studio-importor/src/app/button-controller/export-image.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Button, Tooltip } from 'antd'; -import * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import { FileImageOutlined } from '@ant-design/icons'; -import { Utils } from '@graphscope/studio-components'; - -import { toSvg } from 'html-to-image'; -interface ILeftButtonProps {} - -const ExportImage: React.FunctionComponent = props => { - const onClick = async () => { - // we calculate a transform for the nodes so that all nodes are visible - // we then overwrite the transform of the `.react-flow__viewport` element - // with the style option of the html-to-image library - const viewBox = document.querySelector('.react-flow__viewport') as HTMLDivElement; - if (viewBox) { - const dataUrl = await toSvg(viewBox, {}); - Utils.downloadImage(dataUrl, 'model.svg'); - } - }; - - return ( - } placement="right"> -