├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.js ├── common │ └── Enum.js ├── components │ └── Drawflow │ │ ├── ButtonArea │ │ ├── DrawflowAdditionalArea.js │ │ └── DrawflowZoomArea.js │ │ ├── Connection │ │ ├── Circle.js │ │ ├── Path.js │ │ └── index.js │ │ ├── Drawflow.js │ │ ├── DrawflowNodeBlock.js │ │ ├── Mock │ │ ├── dummy.mock.js │ │ ├── fields.mock.js │ │ ├── index.js │ │ └── rules.mock.js │ │ ├── Modal │ │ ├── ImportModal.js │ │ ├── NodeModal.js │ │ ├── SingleModal.js │ │ ├── ThresholdModal.js │ │ └── index.js │ │ ├── NodeListMenu │ │ ├── FilterList.js │ │ ├── MenuCommonBlock.js │ │ └── RuleList.js │ │ ├── Nodes │ │ ├── Common.js │ │ ├── Round.js │ │ └── index.js │ │ ├── drawflowHandler.js │ │ └── style │ │ └── drawflow.css ├── index.css ├── index.js └── reportWebVitals.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .eslintcache 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ImuruKevol(Taewook Kwon) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-Drawflow 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | And, Convert [Drawflow](https://github.com/jerosoler/Drawflow) for react. 6 | 7 | ## Information 8 | 9 | - Stopped development. 10 | 11 | - Additional is develop for private repository. 12 | - Because, that contains company dependency business logic. 13 | 14 | ## Start 15 | 16 | ### `yarn install` 17 | 18 | After `git clone [url]`, must excute this command. 19 | 20 | ### `yarn start` 21 | 22 | Start hot reloading develop server. 23 | 24 | Default port is `3000`. 25 | 26 | ### `yarn build` 27 | 28 | Create static file. 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-drawflow", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "react": "^17.0.1", 10 | "react-dom": "^17.0.1", 11 | "react-scripts": "4.0.1", 12 | "web-vitals": "^0.2.4" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build" 17 | }, 18 | "eslintConfig": { 19 | "extends": [ 20 | "react-app" 21 | ] 22 | }, 23 | "browserslist": { 24 | "production": [ 25 | ">0.2%", 26 | "not dead", 27 | "not op_mini all" 28 | ], 29 | "development": [ 30 | "last 1 chrome version", 31 | "last 1 firefox version", 32 | "last 1 safari version" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImuruKevol/react-Drawflow/137ca351a7c3bbf729adbbbddc17f5b174fef985/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImuruKevol/react-Drawflow/137ca351a7c3bbf729adbbbddc17f5b174fef985/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImuruKevol/react-Drawflow/137ca351a7c3bbf729adbbbddc17f5b174fef985/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | .App { 6 | width: 1200px; 7 | height: 600px; 8 | margin: 0 auto; 9 | padding-top: 100px; 10 | } 11 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Drawflow from './components/Drawflow/Drawflow'; 3 | import { LIST_TYPE, PAGE, RULES, RULES_LIST_TYPE } from './common/Enum'; 4 | import FilterList from "./components/Drawflow/NodeListMenu/FilterList"; 5 | import RuleList from "./components/Drawflow/NodeListMenu/RuleList"; 6 | import mock from "./components/Drawflow/Mock"; 7 | import './App.css'; 8 | 9 | function App() { 10 | // TODO change logic 11 | const current = window.location.pathname.slice(1).length === 0?RULES.SINGLE:window.location.pathname.slice(1); 12 | //* original data list 13 | const [dataObj, setDataObj] = useState(null); 14 | const [canvasData, setCanvasData] = useState(null); 15 | const [editLock, setEditLock] = useState(false); 16 | const [searchWord, setSearchWord] = useState(""); 17 | 18 | useEffect(() => { 19 | const getInitData = async () => { 20 | let result = null; 21 | const current = window.location.pathname.slice(1).length === 0?RULES.SINGLE:window.location.pathname.slice(1); 22 | switch(RULES_LIST_TYPE[current]) { 23 | case LIST_TYPE.FILTER: 24 | result = await mock.getFilters(PAGE[current]); 25 | break; 26 | case LIST_TYPE.RULE: 27 | result = { 28 | [RULES.SINGLE]: await mock.getSingle(PAGE[current]), 29 | [RULES.THRESHOLD]: await mock.getThreshold(PAGE[current]), 30 | } 31 | break; 32 | default: 33 | break; 34 | } 35 | setDataObj(result); 36 | } 37 | getInitData(); 38 | // TODO type별로 dummy 따로 만들기 39 | mock.getDummy().then(data => { 40 | setCanvasData(data); 41 | }) 42 | }, []); 43 | 44 | const onDragStart = (e, data) => { 45 | e.dataTransfer.setData("data", JSON.stringify(data)); 46 | } 47 | 48 | const isIncludeAndSearch = (target) => { 49 | const arr = searchWord.toLowerCase().split(" ").filter(item => item.length > 0); 50 | return arr.filter(word => target.toLowerCase().includes(word)).length === arr.length; 51 | } 52 | 53 | const useSearchButton = RULES_LIST_TYPE[current] !== LIST_TYPE.FILTER; 54 | 55 | return ( 56 |
57 | {canvasData && dataObj && 58 | <> 59 |
60 |
61 | {setSearchWord(e.target.value)}} 65 | /> 66 | {useSearchButton && } 67 |
68 |
69 | {RULES_LIST_TYPE[current] === LIST_TYPE.FILTER? 70 | : 76 | RULES_LIST_TYPE[current] === LIST_TYPE.RULE? 77 | : 84 | null 85 | } 86 |
87 |
88 | 97 | } 98 |
99 | ); 100 | } 101 | 102 | export default App; 103 | -------------------------------------------------------------------------------- /src/common/Enum.js: -------------------------------------------------------------------------------- 1 | import Nodes from "../components/Drawflow/Nodes"; 2 | 3 | const CURV = 0.5; 4 | 5 | const LIST_TYPE = { 6 | FILTER: "filter", 7 | RULE: "rule", 8 | } 9 | 10 | const RULES = { 11 | SINGLE: "single", 12 | THRESHOLD: "threshold", 13 | CORRELATION: "correlation", 14 | } 15 | 16 | const RULES_LIST_TYPE = { 17 | [RULES.SINGLE]: LIST_TYPE.FILTER, 18 | [RULES.THRESHOLD]: LIST_TYPE.FILTER, 19 | [RULES.CORRELATION]: LIST_TYPE.RULE, 20 | } 21 | 22 | const NODE_BLOCK_TYPE = { 23 | FILTER: "filter", 24 | SINGLE: "single", 25 | THRESHOLD: "threshold", 26 | } 27 | 28 | const PAGE = { 29 | [RULES.SINGLE]: 200, 30 | [RULES.THRESHOLD]: 200, 31 | [RULES.CORRELATION]: 1000, 32 | } 33 | 34 | const MODAL_TYPE = { 35 | import: "import", 36 | common: "common", 37 | [RULES.SINGLE]: "single", 38 | [RULES.THRESHOLD]: "threshold", 39 | [RULES.CORRELATION]: "correlation", 40 | } 41 | 42 | const MODAL_LABEL = { 43 | [MODAL_TYPE.import]: "Import Modal", 44 | [MODAL_TYPE.common]: "Node Modal", 45 | [MODAL_TYPE[RULES.SINGLE]]: "Single Rule Modal", 46 | [MODAL_TYPE[RULES.THRESHOLD]]: "Threshold Rule Modal", 47 | [MODAL_TYPE[RULES.CORRELATION]]: "Correlation Rule Modal", 48 | } 49 | 50 | export { 51 | CURV, 52 | LIST_TYPE, 53 | RULES, 54 | RULES_LIST_TYPE, 55 | MODAL_TYPE, 56 | MODAL_LABEL, 57 | NODE_BLOCK_TYPE, 58 | PAGE, 59 | } 60 | -------------------------------------------------------------------------------- /src/components/Drawflow/ButtonArea/DrawflowAdditionalArea.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const DrawflowAdditionalArea = (props) => { 4 | const { importJson, exportJson, clear, editLock, setEditorMode } = props; 5 | 6 | const changeMode = () => { 7 | setEditorMode(!editLock); 8 | } 9 | 10 | return ( 11 |
12 | {!editLock && 13 | <> 14 | 15 | 16 | 17 | 18 | } 19 | 20 |
21 | ); 22 | } 23 | 24 | export default DrawflowAdditionalArea; 25 | -------------------------------------------------------------------------------- /src/components/Drawflow/ButtonArea/DrawflowZoomArea.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const DrawflowZoomArea = (props) => { 4 | const { zoomIn, zoomOut, zoomReset } = props; 5 | 6 | return ( 7 |
8 | 9 | 10 | 11 |
12 | ); 13 | } 14 | 15 | export default DrawflowZoomArea; 16 | -------------------------------------------------------------------------------- /src/components/Drawflow/Connection/Circle.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CircleComponent = (props) => { 4 | const { property, points, svgKey, i, editLock } = props; 5 | 6 | const onMouseDown = (e) => { 7 | if(editLock) return; 8 | props.select(e, { 9 | svgKey, 10 | i, 11 | }); 12 | } 13 | 14 | const onMouseMove = e => { 15 | if(editLock) return; 16 | props.movePoint(e, svgKey, i); 17 | } 18 | 19 | const onDoubleClick = () => { 20 | if(editLock) return; 21 | const newConnections = points.filter((_, idx) => idx !== i); 22 | props.setConnections(svgKey, newConnections); 23 | } 24 | 25 | return ( 26 | 35 | ); 36 | } 37 | 38 | export default CircleComponent; 39 | -------------------------------------------------------------------------------- /src/components/Drawflow/Connection/Path.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import handler from "../drawflowHandler"; 3 | 4 | const Path = (props) => { 5 | const { editLock, points, zoom, start, end, svgKey, d } = props; 6 | 7 | const customSort = (arrX, arrY, quadrant) => { 8 | let result = []; 9 | let cloneX = [...arrX], cloneY = [...arrY]; 10 | 11 | const pop = (popXY) => { 12 | cloneX = cloneX.filter(item => popXY.x !== item); 13 | cloneY = cloneY.filter(item => popXY.y !== item); 14 | } 15 | const next = () => { 16 | const result = quadrant === 1 ? {x: Math.min(...cloneX), y: Math.min(...cloneY)}: 17 | quadrant === 2 ? {x: Math.max(...cloneX), y: Math.min(...cloneY)}: 18 | quadrant === 3 ? {x: Math.max(...cloneX), y: Math.max(...cloneY)}: 19 | {x: Math.min(...cloneX), y: Math.max(...cloneY)}; 20 | pop(result); 21 | return result; 22 | } 23 | while(cloneX.length > 0) { 24 | result.push(next()); 25 | } 26 | return result; 27 | } 28 | 29 | const sortPoints = (points, start, end) => { 30 | let result = null; 31 | let arrayX = []; 32 | let arrayY = []; 33 | points.reduce((_, val) => { 34 | arrayX.push(val.x); 35 | arrayY.push(val.y); 36 | return null; 37 | }, null); 38 | 39 | if(start.x <= end.x && start.y <= end.y) { 40 | // 1 quadrant 41 | result = customSort(arrayX, arrayY, 1); 42 | } 43 | else if(start.x <= end.x && start.y > end.y) { 44 | // 4 quadrant 45 | result = customSort(arrayX, arrayY, 4); 46 | } 47 | else if(start.x > end.x && start.y <= end.y) { 48 | // 2 quadrant 49 | result = customSort(arrayX, arrayY, 2); 50 | } 51 | else { // start.x > end.x && start.y > end.y 52 | // 3 quadrant 53 | result = customSort(arrayX, arrayY, 3); 54 | } 55 | 56 | return result; 57 | } 58 | 59 | const onMouseDown = e => { 60 | if(editLock) return; 61 | props.select(e, svgKey); 62 | } 63 | 64 | const onDoubleClick = e => { 65 | if(editLock || !svgKey) return; 66 | const pos = handler.getPos(e.clientX, e.clientY, zoom); 67 | const newPoints = sortPoints([...points, pos], start, end); 68 | props.setConnections(svgKey, newPoints); 69 | } 70 | 71 | return ( 72 | 79 | ); 80 | } 81 | 82 | export default Path; 83 | -------------------------------------------------------------------------------- /src/components/Drawflow/Connection/index.js: -------------------------------------------------------------------------------- 1 | import Circle from "./Circle"; 2 | import Path from "./Path"; 3 | 4 | export default { 5 | Circle, 6 | Path 7 | } 8 | -------------------------------------------------------------------------------- /src/components/Drawflow/Drawflow.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import DrawflowAdditionalArea from "./ButtonArea/DrawflowAdditionalArea"; 3 | import DrawflowZoomArea from "./ButtonArea/DrawflowZoomArea"; 4 | import DrawflowNodeBlock from "./DrawflowNodeBlock"; 5 | import Connection from "./Connection"; 6 | import DrawflowModal from "./Modal"; 7 | import Nodes from "./Nodes"; 8 | import handler from "./drawflowHandler"; 9 | import { MODAL_TYPE, MODAL_LABEL, LIST_TYPE, NODE_BLOCK_TYPE } from "../../common/Enum"; 10 | import "./style/drawflow.css"; 11 | 12 | class Drawflow extends React.Component { 13 | constructor () { 14 | super(); 15 | this.state = { 16 | nodeId: 1, 17 | canvasDrag: false, 18 | config: { 19 | drag: false, 20 | connectionsLabelEnable: false, 21 | canvasTranslate: { 22 | x: 0, 23 | y: 0, 24 | }, 25 | zoom: { 26 | value: 1, 27 | max: 2, 28 | min: 0.5, 29 | tick: 0.1, 30 | }, 31 | }, 32 | drawflow: {}, 33 | connections: {}, 34 | connectionsLabel: {}, 35 | ports: {}, 36 | select: null, 37 | selectId: null, 38 | selectPoint: null, 39 | showButton: null, 40 | newPathDirection: null, 41 | modalType: null, 42 | } 43 | this.tmpPorts = {}; 44 | this.NodeComponent = { 45 | [NODE_BLOCK_TYPE.FILTER]: Nodes.Common, 46 | [NODE_BLOCK_TYPE.SINGLE]: Nodes.Round, 47 | [NODE_BLOCK_TYPE.THRESHOLD]: Nodes.Round, 48 | } 49 | } 50 | 51 | /** 52 | * create and add node 53 | * @param {{}} nodeInfo 54 | * @param {{in: Number, out: Number}} port 55 | * @param {{x: Number, y: Number }} pos 56 | * @param {{}} data 57 | */ 58 | addNode = (nodeInfo, port, pos, data = {}) => { 59 | const { nodeId, drawflow } = this.state; 60 | const params = { 61 | id: nodeId, 62 | type: nodeInfo.nodeType, 63 | modalType: nodeInfo.modalType, 64 | data: { 65 | ...data, 66 | create: true, 67 | }, 68 | port, 69 | pos: { 70 | x: pos.x, 71 | y: pos.y, 72 | }, 73 | }; 74 | this.setState({ 75 | nodeId: nodeId + 1, 76 | selectId: nodeId, 77 | drawflow: { 78 | ...drawflow, 79 | [nodeId]: {...params}, 80 | } 81 | }); 82 | } 83 | 84 | getDataByIndex = { 85 | [LIST_TYPE.FILTER]: (idx) => { 86 | return this.props.dataObj.list[idx]; 87 | }, 88 | [LIST_TYPE.RULE]: (idx, type) => { 89 | return this.props.dataObj[type].list[idx]; 90 | }, 91 | } 92 | 93 | addNodeToDrawFlow = (data, x, y) => { 94 | const { type } = this.props; 95 | const { config } = this.state; 96 | if(this.props.editLock) return; 97 | const pos = handler.getPos(x, y, config.zoom.value); 98 | const nodeInfo = {...data}; 99 | delete nodeInfo.index; 100 | delete nodeInfo.menuType; 101 | this.addNode(nodeInfo, {in: 1, out: 1}, pos, this.getDataByIndex[type](data.index, data.menuType)); 102 | } 103 | 104 | drop = (e) => { 105 | e.preventDefault(); 106 | const data = JSON.parse(e.dataTransfer.getData("data")); 107 | this.addNodeToDrawFlow(data, e.clientX, e.clientY); 108 | } 109 | 110 | unSelect = (e) => { 111 | e.stopPropagation(); 112 | const { select, config } = this.state; 113 | if(select) select.classList.remove("select"); 114 | this.setState({ 115 | config: { 116 | ...config, 117 | drag: false, 118 | }, 119 | select: null, 120 | selectId: null, 121 | selectPoint: null, 122 | showButton: null, 123 | }); 124 | } 125 | 126 | select = (e, selectInfo) => { 127 | e.stopPropagation(); 128 | const { config, select } = this.state; 129 | if(select) select.classList.remove("select"); 130 | let target = e.currentTarget; 131 | const isPort = e.target.classList.contains("input") || e.target.classList.contains("output"); 132 | const isNotSeletElement = target.tagName === "circle" || isPort; 133 | if(!isNotSeletElement) 134 | target.classList.add("select"); 135 | if(isPort) target = e.target; 136 | this.setState({ 137 | config: { 138 | ...config, 139 | drag: isPort? false : true, 140 | }, 141 | select: target, 142 | selectId: selectInfo && !selectInfo.svgKey? selectInfo : null, 143 | selectPoint: selectInfo && selectInfo.svgKey? selectInfo : null, 144 | }); 145 | } 146 | 147 | movePoint = (e, svgKey, i) => { 148 | const { config, select } = this.state; 149 | if(!config.drag) return; 150 | if(e.target !== select) return; 151 | const { movementX, movementY } = e; 152 | if(movementX === 0 && movementY === 0) return; 153 | 154 | const { connections } = this.state; 155 | const oldPos = connections[svgKey][i]; 156 | const after = { 157 | x: oldPos.x + movementX, 158 | y: oldPos.y + movementY, 159 | } 160 | let clone = [...connections[svgKey]]; 161 | clone[i] = after; 162 | this.setState({ 163 | connections: { 164 | ...connections, 165 | [svgKey]: clone, 166 | } 167 | }); 168 | } 169 | 170 | setConnections = (svgKey, newConnections) => { 171 | const { connections } = this.state; 172 | this.setState({ 173 | connections: { 174 | ...connections, 175 | [svgKey]: newConnections, 176 | } 177 | }); 178 | } 179 | 180 | drawConnections = (start, end, points, idx, svgKey) => { 181 | const { connections, config } = this.state; 182 | let circles = points.reduce((acc, val, i) => { 183 | const key = "draw-flow-svg-" + idx + "circle-" + i; 184 | const property = { 185 | key, 186 | style: { 187 | cursor: this.props.editLock?"auto":"move", 188 | }, 189 | cx: val.x, 190 | cy: val.y, 191 | } 192 | acc.push( 193 | ); 203 | return acc; 204 | }, []); 205 | 206 | let d = null; 207 | if(points.length > 0) { 208 | let paths = null; 209 | paths = [{start: start, end: points[0], type: "open"}]; 210 | for(let i=0;i { 215 | return acc + handler.createCurvature(val.start, val.end, val.type) + " "; 216 | }, ""); 217 | } 218 | else { 219 | d = handler.createCurvature(start, end, "openclose"); 220 | } 221 | 222 | return ( 223 | <> 224 | 235 | {circles.map(comp => comp)} 236 | 237 | ); 238 | } 239 | 240 | // TODO : label div size에 따라 위치 조정 필요 241 | // TODO : style(z-index, border, background, etc...) 조정 필요 242 | drawConnectionsLabel = (points, label) => { 243 | // calc label position 244 | const pointsLength = points.length; 245 | const mid = Math.floor(pointsLength / 2); 246 | let pos = {}; 247 | if(pointsLength % 2 === 1) { 248 | pos = points[mid]; 249 | } 250 | else { // even 251 | const start = points[mid - 1]; 252 | const end = points[mid]; 253 | pos = { 254 | x: Math.abs(end.x + start.x) / 2, 255 | y: Math.abs(end.y + start.y) / 2, 256 | } 257 | } 258 | 259 | return ( 260 |
268 | {label} 269 |
); 270 | } 271 | 272 | getPortListByNodeId = (nodeId) => { 273 | const { ports } = this.state; 274 | return Object.keys(ports).filter(key => key.split(/_/g)[0] === "" + nodeId); 275 | } 276 | 277 | setPosByNodeId = (nodeId, pos, ports) => { 278 | const { drawflow } = this.state; 279 | this.setState({ 280 | drawflow: { 281 | ...drawflow, 282 | [nodeId]: { 283 | ...drawflow[nodeId], 284 | pos: { 285 | x: pos.x, 286 | y: pos.y, 287 | } 288 | } 289 | }, 290 | ports, 291 | }); 292 | } 293 | 294 | movePosition = (nodeId, pos) => { 295 | const portKeys = this.getPortListByNodeId(nodeId); 296 | const ports = portKeys.reduce((acc, portKey) => { 297 | acc[portKey] = { 298 | x: acc[portKey].x + pos.x, 299 | y: acc[portKey].y + pos.y, 300 | }; 301 | return acc; 302 | }, {...this.state.ports}); 303 | const tmpPos = { 304 | x: this.state.drawflow[nodeId].pos.x + pos.x, 305 | y: this.state.drawflow[nodeId].pos.y + pos.y, 306 | } 307 | this.setPosByNodeId(nodeId, tmpPos, ports); 308 | } 309 | 310 | moveNode = (e, nodeId) => { 311 | const { config, select } = this.state; 312 | if(!config.drag) return; 313 | if(e.currentTarget !== select) return; 314 | const { movementX, movementY } = e; 315 | if(movementX === 0 && movementY === 0) return; 316 | 317 | this.movePosition(nodeId, { 318 | x: movementX, 319 | y: movementY, 320 | }); 321 | } 322 | 323 | setPosWithCursorOut = (e) => { 324 | const { config, selectId, selectPoint } = this.state; 325 | //* typeof selectId === string -> path 326 | const exitCond = (!this.state.select || !config.drag) || (!selectId && !selectPoint) || ((typeof selectId) === (typeof "")); 327 | if(exitCond) return; 328 | 329 | const mousePos = handler.getPos(e.clientX, e.clientY, config.zoom.value); 330 | const select = { 331 | top: this.state.select.style.top.slice(0, -2)*1, 332 | left: this.state.select.style.left.slice(0, -2)*1, 333 | width: this.state.select.clientWidth, 334 | height: this.state.select.clientHeight, 335 | }; 336 | const isInX = mousePos.x >= select.left && mousePos.x <= select.left + select.width; 337 | const isInY = mousePos.y >= select.top && mousePos.y <= select.top + select.height; 338 | if(isInX && isInY) return; 339 | const pos = { 340 | x: mousePos.x - select.width/2 - select.left, 341 | y: mousePos.y - select.height/2 - select.top, 342 | } 343 | if(selectId) this.movePosition(selectId, pos); 344 | else if(selectPoint){ 345 | const { svgKey, i } = selectPoint; 346 | const after = { 347 | x: pos.x, 348 | y: pos.y, 349 | } 350 | let clone = [...this.state.connections[svgKey]]; 351 | clone[i] = after; 352 | this.setState({ 353 | connections: { 354 | ...this.state.connections, 355 | [svgKey]: clone, 356 | } 357 | }); 358 | } 359 | } 360 | 361 | moveCanvas = (e) => { 362 | e.preventDefault(); 363 | e.stopPropagation(); 364 | const { movementX, movementY } = e; 365 | if(movementX === 0 && movementY === 0) return; 366 | this.setState({ 367 | config: { 368 | ...this.state.config, 369 | canvasTranslate: { 370 | x: this.state.config.canvasTranslate.x + movementX, 371 | y: this.state.config.canvasTranslate.y + movementY, 372 | } 373 | } 374 | }); 375 | } 376 | 377 | createPath = (e, startId, startPort, endId, endPort) => { 378 | const { target } = e; 379 | if(!target.classList.contains("input")) return; 380 | const key = `${startId}_${startPort}_${endId}_${endPort}`; 381 | const { connections } = this.state; 382 | if(connections[key] !== undefined) return; 383 | this.setState({ 384 | connections: { 385 | ...this.state.connections, 386 | [key]: [], 387 | } 388 | }); 389 | } 390 | 391 | deleteNode = () => { 392 | if(this.props.editLock) return; 393 | const { connections, drawflow, ports, select, selectId } = this.state; 394 | if(!selectId) return; 395 | let obj = { 396 | connections: {...connections}, 397 | ports: {...ports}, 398 | drawflow: {...drawflow}, 399 | } 400 | // 1. find in connections 401 | Object.keys(obj.connections).reduce((_, val) => { 402 | const arr = val.split("_"); 403 | if(arr[0]*1 === selectId || arr[2]*1 === selectId) { 404 | delete obj.connections[val]; 405 | } 406 | return null; 407 | }, null); 408 | // 2. find in ports 409 | Object.keys(obj.ports).reduce((_, val) => { 410 | const arr = val.split("_"); 411 | if(arr[0]*1 === selectId) { 412 | delete obj.ports[val]; 413 | } 414 | return null; 415 | }, null); 416 | // 3. find in drawflow 417 | delete obj.drawflow[selectId]; 418 | // 4. remove class "select" 419 | if(select) select.classList.remove("select"); 420 | // 5. state clear 421 | obj = { 422 | ...obj, 423 | select: null, 424 | selectId: null, 425 | selectPoint: null, 426 | showButton: null, 427 | } 428 | // 4. set state 429 | this.setState(obj); 430 | } 431 | 432 | pathDelete = () => { 433 | if(this.props.editLock) return; 434 | const { selectId, connections } = this.state; 435 | let newConnections = {...connections}; 436 | delete newConnections[selectId]; 437 | this.setState({ 438 | connections: newConnections, 439 | }); 440 | } 441 | 442 | pushPorts = (ports) => { 443 | this.tmpPorts = { 444 | ...this.tmpPorts, 445 | ...this.state.ports, 446 | ...ports, 447 | } 448 | this.setState({ 449 | ports: { 450 | ...this.state.ports, 451 | ...this.tmpPorts, 452 | } 453 | }); 454 | } 455 | 456 | onMouseMoveCanvas = (e) => { 457 | const { canvasDrag } = this.state; 458 | if(canvasDrag) this.moveCanvas(e); 459 | 460 | const { select } = this.state; 461 | if(select && select.classList.contains("output")) { 462 | const { clientX, clientY } = e; 463 | 464 | this.setState({ 465 | newPathDirection: { 466 | clientX, 467 | clientY, 468 | }, 469 | }); 470 | } 471 | this.setPosWithCursorOut(e); 472 | } 473 | 474 | onMouseDownCanvas = e => { 475 | if(e.target.id !== "drawflow" && !e.target.classList.contains("drawflow")) return; 476 | this.setState({ 477 | canvasDrag: true, 478 | }); 479 | this.unSelect(e); 480 | } 481 | 482 | onMouseUpCanvas = e => { 483 | let obj = { 484 | newPathDirection: null, 485 | canvasDrag: false, 486 | config: { 487 | ...this.state.config, 488 | drag: false, 489 | } 490 | } 491 | const { select } = this.state; 492 | if(select && select.classList.contains("output")) { 493 | obj.select = null; 494 | } 495 | this.setState(obj); 496 | } 497 | 498 | onKeyDown = (e) => { 499 | if(e.key === "Delete"){ 500 | const { select } = this.state; 501 | if(select && select.tagName === "path") { 502 | this.pathDelete(); 503 | } 504 | else { 505 | this.deleteNode(); 506 | } 507 | } 508 | } 509 | 510 | onChangeSearchWord = e => { 511 | this.props.setSearchWord({ 512 | searchWord: e.target.value, 513 | }); 514 | } 515 | 516 | load = async (data) => { 517 | const { dataObj } = this.props; 518 | const { connections } = data; 519 | if(!dataObj || !connections) return; 520 | 521 | let obj = { 522 | connections, 523 | drawflow: data.nodes, 524 | config: { 525 | ...this.state.config, 526 | } 527 | }; 528 | 529 | if(data.connectionsLabel) { 530 | obj.connectionsLabel = data.connectionsLabel; 531 | obj.config.connectionsLabelEnable = true; 532 | } 533 | 534 | const dataKeys = Object.keys(data.nodes).map(key => key*1).sort(); 535 | if(dataKeys.length > 0) { 536 | obj.nodeId = dataKeys.slice(-1)*1 + 1; 537 | } 538 | 539 | this.setState({ 540 | ...obj, 541 | }); 542 | } 543 | 544 | newPath = () => { 545 | const { select, config, ports, selectId, newPathDirection } = this.state; 546 | const idx = handler.findIndexByElement(select); 547 | const startKey = `${selectId}_out_${idx + 1}`; 548 | 549 | if(!ports[startKey]) return null; 550 | 551 | const start = { 552 | x: ports[startKey].x, 553 | y: ports[startKey].y, 554 | } 555 | const zoom = config.zoom.value; 556 | const { clientX, clientY } = newPathDirection; 557 | const end = handler.getPos(clientX, clientY, zoom); 558 | const d = handler.createCurvature(start, end, "openclose"); 559 | 560 | return ( 561 | 565 | 574 | 575 | ); 576 | } 577 | 578 | /* Life Cycle Function Start */ 579 | componentDidMount() { 580 | if(this.props.canvasData) { 581 | this.load(this.props.canvasData); 582 | document.addEventListener("keydown", this.onKeyDown); 583 | } 584 | } 585 | 586 | componentWillUnmount() { 587 | document.removeEventListener("keydown", this.onKeyDown); 588 | } 589 | /* Life Cycle Function End */ 590 | 591 | /* Button Function Area Start */ 592 | importJson = () => { 593 | this.setState({ 594 | modalType: MODAL_TYPE.import, 595 | }); 596 | } 597 | 598 | exportJson = () => { 599 | const { drawflow, connections, connectionsLabel, config } = this.state; 600 | const nodes = Object.entries(drawflow).reduce((acc, [nodeId, data]) => { 601 | return { 602 | ...acc, 603 | [nodeId]: data, 604 | } 605 | }, {}); 606 | const exportData = Object.assign({ 607 | nodes, 608 | connections, 609 | }, config.connectionsLabelEnable?{connectionsLabel}:{}); 610 | if(!navigator.clipboard || !navigator.clipboard.writeText){ 611 | alert("clipboard api를 지원하지 않는 브라우저입니다."); 612 | return; 613 | } 614 | navigator.clipboard.writeText(JSON.stringify(exportData, null, 2)).then(() => { 615 | alert("json 데이터가 클립보드에 저장되었습니다."); 616 | }); 617 | } 618 | 619 | clear = () => { 620 | this.setState({ 621 | nodeId: 1, 622 | config: { 623 | ...this.state.config, 624 | canvasTranslate: { 625 | x: 0, 626 | y: 0, 627 | }, 628 | zoom: { 629 | ...this.state.config.zoom, 630 | value: 1, 631 | }, 632 | }, 633 | drawflow: {}, 634 | connections: {}, 635 | ports: {}, 636 | select: null, 637 | selectId: null, 638 | selectPoint: null, 639 | showButton: null, 640 | newPathDirection: null, 641 | modalType: null, 642 | }); 643 | } 644 | 645 | /** 646 | * @param {Boolean} plag true: zoom in, false: zoom out, null: zoom reset 647 | */ 648 | zoom = (plag) => { 649 | const { zoom } = this.state.config; 650 | const { value, max, min, tick } = zoom; 651 | let afterZoom = plag? value + tick : value - tick; 652 | let obj = { 653 | zoom: { 654 | ...zoom, 655 | value: afterZoom, 656 | } 657 | } 658 | if(plag === null) { 659 | obj.zoom.value = 1; 660 | obj.canvasTranslate = { 661 | x: 0, 662 | y: 0, 663 | } 664 | } 665 | if(afterZoom > max || afterZoom < min) return; 666 | this.setState({ 667 | config: { 668 | ...this.state.config, 669 | ...obj, 670 | } 671 | }); 672 | } 673 | /* Button Function Area End */ 674 | 675 | render () { 676 | const nodeBlockEvent = this.props.editLock? 677 | { 678 | select: () => {}, 679 | moveNode: () => {}, 680 | createPath: () => {}, 681 | deleteNode: () => {}, 682 | } 683 | : 684 | { 685 | select: this.select, 686 | moveNode: this.moveNode, 687 | createPath: (e, endId, endPort) => { 688 | const { selectId, select } = this.state; 689 | if(selectId === endId) return; 690 | const startPort = handler.findIndexByElement(select) + 1; 691 | this.createPath(e, selectId, startPort, endId, endPort); 692 | }, 693 | deleteNode: this.deleteNode, 694 | }; 695 | 696 | return ( 697 |
698 | {this.state.modalType && 699 | { 704 | const { drawflow, selectId } = this.state; 705 | this.setState({ 706 | drawflow: { 707 | ...drawflow, 708 | [selectId]: { 709 | ...drawflow[selectId], 710 | data: data, 711 | } 712 | } 713 | }); 714 | }} 715 | close={() => { 716 | this.setState({ 717 | modalType: null, 718 | }); 719 | }} 720 | event={{ 721 | importData: (data) => { 722 | try { 723 | this.load(data); 724 | } 725 | catch{ 726 | alert("Is not regular format."); 727 | } 728 | }, 729 | deleteNode: this.deleteNode, 730 | }} 731 | /> 732 | } 733 |
734 |
735 |
{e.preventDefault()}} 743 | > 744 | 751 | {/* deactive */} 752 | {/* */} 757 |
763 | {Object.values(this.state.drawflow).map((node, idx) => 764 | { 774 | this.setState({ 775 | showButton: nodeId, 776 | }); 777 | }} 778 | showModal={(type) => { 779 | this.setState({ 780 | modalType: type, 781 | }); 782 | }} 783 | event={nodeBlockEvent} 784 | /> 785 | )} 786 | {Object.entries(this.state.connections).map(([key, points], idx) => { 787 | // key: fromId_portNum_toId_portNum 788 | const { ports, connectionsLabel, config } = this.state; 789 | const arr = key.split("_"); 790 | const startKey = `${arr[0]}_out_${arr[1]}`; 791 | const endKey = `${arr[2]}_in_${arr[3]}`; 792 | 793 | if(!ports[startKey] || !ports[endKey]) return null; 794 | 795 | const start = { 796 | x: ports[startKey].x, 797 | y: ports[startKey].y, 798 | } 799 | const end = { 800 | x: ports[endKey].x, 801 | y: ports[endKey].y, 802 | } 803 | return ( 804 | <> 805 | 810 | {this.drawConnections(start, end, points, idx, key)} 811 | 812 | {config.connectionsLabelEnable && 813 |
814 | {this.drawConnectionsLabel([start, ...points, end], connectionsLabel[key])} 815 |
} 816 | 817 | ); 818 | })} 819 | {this.state.newPathDirection && this.newPath()} 820 |
821 |
822 |
823 |
824 |
825 | ); 826 | } 827 | } 828 | 829 | export default Drawflow; 830 | -------------------------------------------------------------------------------- /src/components/Drawflow/DrawflowNodeBlock.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useRef } from "react"; 2 | import { NODE_BLOCK_TYPE } from "../../common/Enum"; 3 | import handler from "./drawflowHandler"; 4 | 5 | const DrawflowNodeBlock = ({ 6 | zoom, 7 | NodeContent, 8 | params, 9 | editLock, 10 | ports, 11 | pushPorts, 12 | showButton, 13 | setShowButton, 14 | showModal, 15 | event, 16 | }) => { 17 | const [refs, setRefs] = useState({ 18 | inputs: [], 19 | outputs: [], 20 | }); 21 | const ref = useRef(null); 22 | 23 | const getPortPosWithZoom = (size, pos) => { 24 | const canvas = handler.getCanvasInfo(); 25 | const widthZoom = (canvas.width / (canvas.width * zoom)) || 0; 26 | const heightZoom = (canvas.height / (canvas.height * zoom)) || 0; 27 | const x = size.width/2 + (pos.x - canvas.x ) * widthZoom; 28 | const y = size.height/2 + (pos.y - canvas.y ) * heightZoom; 29 | 30 | return {x, y}; 31 | } 32 | 33 | const portComponent = (type) => { 34 | let arr = []; 35 | 36 | for(let i=1;i<=params.port[type];i++) { 37 | const port = 38 |
{ 42 | event.createPath(e, params.id, i); 43 | }} 44 | >
; 45 | arr.push(port); 46 | } 47 | 48 | return ( 49 |
50 | {arr.map(ele => ele)} 51 |
52 | ); 53 | } 54 | 55 | useEffect(() => { 56 | if(ref.current) { 57 | const inputs = Array.from(ref.current.querySelector(".inputs").children); 58 | const outputs = Array.from(ref.current.querySelector(".outputs").children); 59 | setRefs({ 60 | inputs, 61 | outputs, 62 | }); 63 | } 64 | }, [ref]); 65 | 66 | const getPortPos = (type, i, elmt) => { 67 | const key = `${params.id}_${type}_${i}`; 68 | if(!ports[key]) { 69 | const rect = elmt.getBoundingClientRect(); 70 | const size = { 71 | width: elmt.offsetWidth, 72 | height: elmt.offsetHeight, 73 | }; 74 | const pos = { 75 | x: rect.x, 76 | y: rect.y, 77 | }; 78 | return { 79 | [key]: getPortPosWithZoom(size, pos), 80 | } 81 | } 82 | } 83 | 84 | useEffect(() => { 85 | if(refs.inputs && refs.outputs && params.port.in === refs.inputs.length && params.port.out === refs.outputs.length) { 86 | let newPorts = {}; 87 | newPorts = Object.assign(newPorts, refs.inputs.reduce((acc, elmt, i) => { 88 | return Object.assign(acc, getPortPos("in", i + 1, elmt)); 89 | }, {})); 90 | newPorts = Object.assign(newPorts, refs.outputs.reduce((acc, elmt, i) => { 91 | return Object.assign(acc, getPortPos("out", i + 1, elmt)); 92 | }, {})); 93 | pushPorts(newPorts); 94 | } 95 | }, [refs]); 96 | 97 | useEffect(() => { 98 | const isShowModalByCreate = params.type === NODE_BLOCK_TYPE.FILTER; 99 | if(params.data.create && isShowModalByCreate) { 100 | showModal(params.modalType); 101 | } 102 | }, [params.data]); 103 | 104 | const className = `drawflow-node-block-${params.type.replace(/\s/g, "").toLowerCase()}`; 105 | 106 | return ( 107 | // If you want, change styled component. My case is not supported styled component... 108 | <> 109 |
{ 118 | if(e.currentTarget.classList.contains(className)) { 119 | event.select(e, params.id); 120 | } 121 | }} 122 | onMouseMove={e => { 123 | event.moveNode(e, params.id); 124 | }} 125 | onContextMenu={e => { 126 | e.preventDefault(); 127 | e.stopPropagation(); 128 | setShowButton(params.id); 129 | }} 130 | onDoubleClick={() => { 131 | showModal(params.modalType); 132 | }} 133 | > 134 | {portComponent("in")} 135 |
138 | 141 |
142 | {portComponent("out")} 143 | 150 |
151 | 152 | ); 153 | } 154 | 155 | export default DrawflowNodeBlock; 156 | -------------------------------------------------------------------------------- /src/components/Drawflow/Mock/dummy.mock.js: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return new Promise((resolve) => { 3 | resolve({ 4 | "nodes": { 5 | "1": { 6 | "id": 1, 7 | "type": "filter", 8 | "modalType": "common", 9 | "data": { 10 | "type": "Numeric", 11 | "name": "HLuF7rwKIuD", 12 | "value": "asdf" 13 | }, 14 | "port": { 15 | "in": 1, 16 | "out": 1 17 | }, 18 | "pos": { 19 | "x": 43.3125, 20 | "y": 14 21 | } 22 | }, 23 | "2": { 24 | "id": 2, 25 | "type": "filter", 26 | "modalType": "common", 27 | "data": { 28 | "type": "String", 29 | "name": "y24mqVYQtD", 30 | "value": "eeee" 31 | }, 32 | "port": { 33 | "in": 1, 34 | "out": 1 35 | }, 36 | "pos": { 37 | "x": 469.3125, 38 | "y": 286 39 | } 40 | }, 41 | "3": { 42 | "id": 3, 43 | "type": "filter", 44 | "modalType": "common", 45 | "data": { 46 | "type": "String", 47 | "name": "y24mqVYQtD", 48 | "value": "asdffff" 49 | }, 50 | "port": { 51 | "in": 1, 52 | "out": 1 53 | }, 54 | "pos": { 55 | "x": 436.8125, 56 | "y": 92 57 | } 58 | }, 59 | "4": { 60 | "id": 4, 61 | "type": "filter", 62 | "modalType": "common", 63 | "data": { 64 | "type": "String", 65 | "name": "1qdlCNXqYBsE", 66 | "value": "qqweee" 67 | }, 68 | "port": { 69 | "in": 1, 70 | "out": 1 71 | }, 72 | "pos": { 73 | "x": 36.3125, 74 | "y": 209 75 | } 76 | } 77 | }, 78 | "connections": { 79 | "1_1_3_1": [ 80 | { 81 | "x": 327.3125, 82 | "y": 327 83 | } 84 | ], 85 | "4_1_2_1": [ 86 | { 87 | "x": 323.3125, 88 | "y": 57 89 | } 90 | ] 91 | }, 92 | "connectionsLabel": { 93 | "1_1_3_1": "test label", 94 | } 95 | }); 96 | }); 97 | } 98 | -------------------------------------------------------------------------------- /src/components/Drawflow/Mock/fields.mock.js: -------------------------------------------------------------------------------- 1 | import { MODAL_TYPE, LIST_TYPE } from "../../../common/Enum"; 2 | 3 | const types = ["String", "Numeric", "IP"]; 4 | const modalType = MODAL_TYPE.common; 5 | 6 | 7 | const makeRandomNames = (length, searchWord, max = 15, min = 5) => { 8 | const result = []; 9 | const map = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 10 | for(let j=0;j 1) { 16 | word += searchWord; 17 | } 18 | result.push(word); 19 | } 20 | return result; 21 | } 22 | 23 | export default async (number, searchWord = "") => { 24 | let names = makeRandomNames(number, searchWord); 25 | 26 | return { 27 | type: LIST_TYPE.FILTER, 28 | modalType, 29 | list: names.reduce((acc, val) => { 30 | acc.push({ 31 | type: types[Math.floor(Math.random() * types.length)], 32 | name: val, 33 | value: makeRandomNames(1, "", 10, 5)[0], 34 | }); 35 | return acc; 36 | }, []), 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Drawflow/Mock/index.js: -------------------------------------------------------------------------------- 1 | import getDummy from "./dummy.mock"; 2 | import getFilters from "./fields.mock"; 3 | import { 4 | getSingle, 5 | getThreshold, 6 | } from "./rules.mock"; 7 | 8 | export default { 9 | getDummy, 10 | getFilters, 11 | getSingle, 12 | getThreshold, 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Drawflow/Mock/rules.mock.js: -------------------------------------------------------------------------------- 1 | import { MODAL_TYPE } from "../../../common/Enum"; 2 | 3 | /** 4 | * type: field | rule 5 | * value type: type field -> String | Numeric | IP , type rule -> Single | Threshold 6 | * name 7 | * modal type(node dbl click): user custom 8 | * addon(optional) 9 | */ 10 | 11 | const isInludeAndSearch = (searchWord, target) => { 12 | const arr = searchWord.toLowerCase().split(" ").filter(item => item.length > 0); 13 | return arr.filter(word => target.toLowerCase().includes(word)).length === arr.length; 14 | } 15 | 16 | const makeRandomNames = (length, searchWord, max = 15, min = 5) => { 17 | const result = []; 18 | const map = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 19 | for(let j=0;j { 32 | let names = makeRandomNames(number, searchWord); 33 | return { 34 | type: "rule_single", 35 | modalType: MODAL_TYPE.single, 36 | list: names.reduce((acc, val) => { 37 | acc.push({ 38 | name: val, 39 | }); 40 | return acc; 41 | }, []), 42 | }; 43 | } 44 | 45 | 46 | const getThreshold = async (number, searchWord = "") => { 47 | let names = makeRandomNames(number, searchWord); 48 | return { 49 | type: "rule_threshold", 50 | modalType: MODAL_TYPE.threshold, 51 | list: names.reduce((acc, val) => { 52 | acc.push({ 53 | name: val, 54 | }); 55 | return acc; 56 | }, []), 57 | }; 58 | } 59 | 60 | export { 61 | getSingle, 62 | getThreshold, 63 | } 64 | -------------------------------------------------------------------------------- /src/components/Drawflow/Modal/ImportModal.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | const ImportModal = (props) => { 4 | const { title, close, importData } = props; 5 | const [importType, setImportType] = useState(true); 6 | const [json, setJson] = useState(""); 7 | 8 | const getJson = (e) => { 9 | const { files } = e.target; 10 | if(files.length === 0) return; 11 | const file = files[0]; 12 | if(file.name.split(".").slice(-1)[0].toLowerCase() !== "json") return; 13 | const fileReader = new FileReader(); 14 | fileReader.addEventListener("load", e => { 15 | setJson(e.target.result); 16 | }) 17 | fileReader.readAsText(file); 18 | } 19 | 20 | return ( 21 |
22 |
23 | {title} 24 | 25 |
26 |
27 | 28 | 41 | 42 | 43 | 56 | 57 |
58 | {importType? 59 |
60 | 69 |
70 | : 71 |
72 | 73 |
74 | } 75 |
76 | 87 |
88 |
89 | ); 90 | } 91 | 92 | export default ImportModal; 93 | -------------------------------------------------------------------------------- /src/components/Drawflow/Modal/NodeModal.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | 3 | const NodeModal = (props) => { 4 | const { title, close, data, setData } = props; 5 | const [value, setValue] = useState(""); 6 | console.log(data) 7 | 8 | return ( 9 |
10 |
11 | {title} 12 | 21 |
22 |
23 | this is node modal.
24 | Name: {data.name}
25 |
26 | { 30 | e.stopPropagation(); 31 | }} 32 | onChange={e => { 33 | setValue(e.target.value); 34 | }} 35 | /> 36 |
37 | 46 | 52 |
53 |
54 | ); 55 | } 56 | 57 | export default NodeModal; 58 | -------------------------------------------------------------------------------- /src/components/Drawflow/Modal/SingleModal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const NodeModal = (props) => { 4 | const { title, close, data, setData } = props; 5 | console.log(data) 6 | 7 | return ( 8 |
9 |
10 | {title} 11 | 12 |
13 |
14 | this is single modal.
15 | Name: {data.name} 16 |
17 |
18 | ); 19 | } 20 | 21 | export default NodeModal; 22 | -------------------------------------------------------------------------------- /src/components/Drawflow/Modal/ThresholdModal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const NodeModal = (props) => { 4 | const { title, close, data, setData } = props; 5 | console.log(data) 6 | 7 | return ( 8 |
9 |
10 | {title} 11 | 12 |
13 |
14 | this is threshold modal.
15 | Name: {data.name} 16 |
17 |
18 | ); 19 | } 20 | 21 | export default NodeModal; 22 | -------------------------------------------------------------------------------- /src/components/Drawflow/Modal/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ImportModal from "./ImportModal"; 3 | import NodeModal from "./NodeModal"; 4 | import SingleModal from "./SingleModal"; 5 | import ThresholdModal from "./ThresholdModal"; 6 | import { MODAL_TYPE } from "../../../common/Enum"; 7 | 8 | const modalMap = { 9 | [MODAL_TYPE.import]: ImportModal, 10 | [MODAL_TYPE.common]: NodeModal, 11 | [MODAL_TYPE.single]: SingleModal, 12 | [MODAL_TYPE.threshold]: ThresholdModal, 13 | } 14 | 15 | const DrawflowModal = (props) => { 16 | const { type, close, title, data, setData, event } = props; 17 | const Component = modalMap[type]; 18 | return ( 19 |
20 | 27 |
28 | ); 29 | } 30 | 31 | export default DrawflowModal; 32 | -------------------------------------------------------------------------------- /src/components/Drawflow/NodeListMenu/FilterList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import MenuCommonBlock from "./MenuCommonBlock"; 3 | import { NODE_BLOCK_TYPE } from "../../../common/Enum"; 4 | 5 | const FilterList = (props) => { 6 | const { filterObj, editLock, onDragStart, isIncludeAndSearch } = props; 7 | 8 | return ( 9 |
12 | {filterObj.list.map((item, idx) => { 13 | const label = `[${item.type.slice(0, 1)}] ${item.name}`; 14 | return ( 15 | isIncludeAndSearch(label) && 16 | { 21 | onDragStart(e, { 22 | nodeType: NODE_BLOCK_TYPE.FILTER, 23 | index: idx, 24 | modalType: filterObj.modalType, 25 | }); 26 | }} 27 | />); 28 | })} 29 |
30 | ); 31 | } 32 | 33 | export default FilterList; 34 | -------------------------------------------------------------------------------- /src/components/Drawflow/NodeListMenu/MenuCommonBlock.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const MenuCommonBlock = (props) => { 4 | const { label, editLock, onDragStart } = props; 5 | 6 | return ( 7 |
12 | {label} 13 |
14 | ); 15 | } 16 | 17 | export default MenuCommonBlock; 18 | -------------------------------------------------------------------------------- /src/components/Drawflow/NodeListMenu/RuleList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import MenuCommonBlock from "./MenuCommonBlock"; 3 | import { RULES, NODE_BLOCK_TYPE } from "../../../common/Enum"; 4 | 5 | const RuleList = (props) => { 6 | const { single, threshold, editLock, onDragStart, isIncludeAndSearch } = props; 7 | return ( 8 | <> 9 |
10 |
Single
11 |
12 | {single.list.slice(0, 3000).map((item, idx) => { 13 | const label = `[${10001 + idx}] ${item.name}`; 14 | return ( 15 | isIncludeAndSearch(label) && 16 | { 21 | onDragStart(e, { 22 | nodeType: NODE_BLOCK_TYPE.SINGLE, 23 | index: idx, 24 | menuType: RULES.SINGLE, 25 | modalType: single.modalType 26 | }); 27 | }} 28 | />); 29 | })} 30 |
31 |
32 |
33 |
Threshold
34 |
35 | {threshold.list.slice(0, 3000).map((item, idx) => { 36 | const label = `[${10001 + idx}] ${item.name}`; 37 | return ( 38 | isIncludeAndSearch(label) && 39 | { 44 | onDragStart(e, { 45 | nodeType: NODE_BLOCK_TYPE.THRESHOLD, 46 | index: idx, 47 | menuType: RULES.THRESHOLD, 48 | modalType: threshold.modalType, 49 | }); 50 | }} 51 | />); 52 | })} 53 |
54 |
55 | 56 | ); 57 | } 58 | 59 | export default RuleList; 60 | -------------------------------------------------------------------------------- /src/components/Drawflow/Nodes/Common.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Common = (props) => { 4 | const { data } = props; 5 | 6 | return ( 7 | <> 8 | {`${data.type?`[${data.type.slice(0, 1)}]`:""}${data.name}`} 9 |
{data.value}
10 | ); 11 | } 12 | 13 | export default Common; 14 | -------------------------------------------------------------------------------- /src/components/Drawflow/Nodes/Round.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Round = (props) => { 4 | const { type, data } = props; 5 | 6 | return ( 7 | <> 8 |
Type: {type}
9 | {data.name} 10 | 11 | ); 12 | } 13 | 14 | export default Round; 15 | -------------------------------------------------------------------------------- /src/components/Drawflow/Nodes/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-anonymous-default-export */ 2 | import Common from "./Common"; 3 | import Round from "./Round"; 4 | 5 | export default { 6 | Common, 7 | Round, 8 | }; 9 | -------------------------------------------------------------------------------- /src/components/Drawflow/drawflowHandler.js: -------------------------------------------------------------------------------- 1 | import {CURV as curv} from "../../common/Enum"; 2 | 3 | const createCurvature = (start, end, type) => { 4 | let hx1 = null; 5 | let hx2 = null; 6 | 7 | //type openclose open close other 8 | switch (type) { 9 | case 'open': 10 | if (start.x >= end.x) { 11 | hx1 = start.x + Math.abs(end.x - start.x) * curv; 12 | hx2 = end.x - Math.abs(end.x - start.x) * (curv * -1); 13 | } else { 14 | hx1 = start.x + Math.abs(end.x - start.x) * curv; 15 | hx2 = end.x - Math.abs(end.x - start.x) * curv; 16 | } 17 | return ' M ' + start.x + ' ' + start.y + ' C ' + hx1 + ' ' + start.y + ' ' + hx2 + ' ' + end.y + ' ' + end.x + ' ' + end.y; 18 | 19 | case 'close': 20 | if (start.x >= end.x) { 21 | hx1 = start.x + Math.abs(end.x - start.x) * (curv * -1); 22 | hx2 = end.x - Math.abs(end.x - start.x) * curv; 23 | } else { 24 | hx1 = start.x + Math.abs(end.x - start.x) * curv; 25 | hx2 = end.x - Math.abs(end.x - start.x) * curv; 26 | } 27 | return ' M ' + start.x + ' ' + start.y + ' C ' + hx1 + ' ' + start.y + ' ' + hx2 + ' ' + end.y + ' ' + end.x + ' ' + end.y; 28 | 29 | case 'other': 30 | if (start.x >= end.x) { 31 | hx1 = start.x + Math.abs(end.x - start.x) * (curv * -1); 32 | hx2 = end.x - Math.abs(end.x - start.x) * (curv * -1); 33 | } else { 34 | hx1 = start.x + Math.abs(end.x - start.x) * curv; 35 | hx2 = end.x - Math.abs(end.x - start.x) * curv; 36 | } 37 | return ' M ' + start.x + ' ' + start.y + ' C ' + hx1 + ' ' + start.y + ' ' + hx2 + ' ' + end.y + ' ' + end.x + ' ' + end.y; 38 | 39 | default: 40 | hx1 = start.x + Math.abs(end.x - start.x) * curv; 41 | hx2 = end.x - Math.abs(end.x - start.x) * curv; 42 | 43 | return ' M ' + start.x + ' ' + start.y + ' C ' + hx1 + ' ' + start.y + ' ' + hx2 + ' ' + end.y + ' ' + end.x + ' ' + end.y; 44 | } 45 | } 46 | 47 | const getCanvasInfo = () => { 48 | // TODO : replace querySelector to someting 49 | const canvas = document.querySelector("#drawflow").querySelector(".drawflow"); 50 | const canvasRect = canvas.getBoundingClientRect(); 51 | return { 52 | x: canvasRect.x, 53 | y: canvasRect.y, 54 | width: canvas.clientWidth, 55 | height: canvas.clientHeight, 56 | }; 57 | } 58 | 59 | const getPos = (clientX, clientY, zoom) => { 60 | const { x, y, width, height } = getCanvasInfo(); 61 | return { 62 | x: clientX * (width / (width * zoom)) - (x * (width / (width * zoom))), 63 | y: clientY * (height / (height * zoom)) - (y * (height / (height * zoom))), 64 | } 65 | } 66 | 67 | const findIndexByElement = (elmt) => { 68 | const { parentElement } = elmt; 69 | const arr = Array.from(parentElement.childNodes); 70 | 71 | for(let i=0;i .drawflow-node-list-category-wrap { 52 | height: 50%; 53 | border-bottom: 1px solid black; 54 | padding-top: 30px; 55 | } 56 | 57 | .drawflow-container { 58 | display: inline-block; 59 | width: calc(100% - var(--sidemenu-width)); 60 | height: 100%; 61 | } 62 | 63 | .drawflow-wrapper { 64 | height: 100%; 65 | display: flex; 66 | } 67 | 68 | .drawflow-main { 69 | width: 100%; 70 | } 71 | 72 | .drawflow-main #drawflow .drawflow .inputs .input, 73 | .drawflow-main #drawflow .drawflow .outputs .output { 74 | height: 15px; 75 | width: 15px; 76 | border: 2px solid var(--border-color); 77 | } 78 | 79 | .drawflow-main #drawflow .drawflow .inputs .input:hover, 80 | .drawflow-main #drawflow .drawflow .outputs .output:hover { 81 | background: #4ea9ff; 82 | } 83 | 84 | .drawflow-main #drawflow .drawflow .inputs { 85 | position: absolute; 86 | left: -8px; 87 | background-color: #777; 88 | } 89 | 90 | .drawflow-main #drawflow .drawflow .outputs { 91 | position: absolute; 92 | right: -8px; 93 | background-color: #777; 94 | } 95 | 96 | .drawflow-main #drawflow .drawflow path:hover { 97 | stroke-width: 6px; 98 | stroke: purple; 99 | } 100 | 101 | .drawflow-main #drawflow .drawflow .select, 102 | .drawflow-main #drawflow .drawflow path.select:hover { 103 | stroke-width: 7px; 104 | stroke: red; 105 | } 106 | 107 | #drawflow { 108 | display: flex; 109 | position: relative; 110 | width: 100%; 111 | height: 100%; 112 | overflow: hidden; 113 | background: var(--background-color); 114 | background-size: 25px 25px; 115 | background-image: 116 | linear-gradient(to right, var(--background-plaid-color) 1px, transparent 1px), 117 | linear-gradient(to bottom, var(--background-plaid-color) 1px, transparent 1px); 118 | outline:none; 119 | } 120 | 121 | .drawflow { 122 | width: 100%; 123 | height: 100%; 124 | position: relative; 125 | user-select: none; 126 | } 127 | 128 | .drawflow .drawflow-node-block-default.select { 129 | -webkit-box-shadow: 0 2px 15px 2px var(--border-color); 130 | box-shadow: 0 2px 15px 2px var(--border-color); 131 | border: 2px solid blue; 132 | z-index: 30; 133 | } 134 | 135 | .drawflow .point.select { 136 | r: 7; 137 | fill: red; 138 | z-index: 30; 139 | } 140 | 141 | .drawflow .parent-node { 142 | position: relative; 143 | 144 | } 145 | 146 | .drawflow svg { 147 | z-index: 10; 148 | position: absolute; 149 | overflow: visible !important; 150 | } 151 | .drawflow .drawflow-connection { 152 | position: absolute; 153 | transform: translate(9999px, 9999px); 154 | } 155 | .drawflow .drawflow-connection .main-path { 156 | fill: none; 157 | stroke-width: 5px; 158 | stroke: steelblue; 159 | transform: translate(-9999px, -9999px); 160 | } 161 | .drawflow .drawflow-connection .main-path:hover { 162 | stroke: #1266ab; 163 | cursor: pointer; 164 | } 165 | 166 | .drawflow .drawflow-connection .main-path.selected { 167 | stroke: #43b993; 168 | } 169 | 170 | .drawflow .drawflow-connection .point { 171 | stroke: black; 172 | stroke-width: 2; 173 | fill: white; 174 | transform: translate(-9999px, -9999px); 175 | } 176 | 177 | .drawflow .drawflow-connection .point.selected, .drawflow .drawflow-connection .point:hover { 178 | fill: #1266ab; 179 | } 180 | 181 | .drawflow .input.select, 182 | .drawflow .output.select { 183 | background-color: yellowgreen; 184 | } 185 | 186 | .drawflow .main-path { 187 | fill: none; 188 | stroke-width: 5px; 189 | stroke: steelblue; 190 | } 191 | 192 | .drawflow-node-block { 193 | line-height: 35px; 194 | border-bottom: 1px solid var(--border-color); 195 | padding: 0 5px; 196 | cursor: move; 197 | user-select: none; 198 | } 199 | 200 | .drawflow-additional { 201 | float: right; 202 | position: absolute; 203 | top: 10px; 204 | right: 10px; 205 | padding: 5px 10px; 206 | background-color: #ddd; 207 | font-weight: bold; 208 | z-index: 10; 209 | } 210 | 211 | .drawflow-additional .drawflow-additional-button { 212 | background-color: #333; 213 | color: #fff; 214 | border-radius: 4px; 215 | } 216 | 217 | .drawflow-zoom { 218 | float: right; 219 | position: absolute; 220 | bottom: 10px; 221 | right: 10px; 222 | padding: 5px 10px; 223 | background-color: #ddd; 224 | font-weight: bold; 225 | z-index: 10; 226 | } 227 | 228 | .drawflow-zoom-button { 229 | background-color: #333; 230 | color: #fff; 231 | border-radius: 4px; 232 | padding: 3px 10px; 233 | } 234 | 235 | .drawflow-node-block-default { 236 | display: inline-block; 237 | padding: 10px 15px; 238 | position: absolute; 239 | border: 1px solid black; 240 | display: flex; 241 | align-items: center; 242 | z-index: 20; 243 | } 244 | 245 | .drawflow-node-block-default { 246 | background-color: lightgray; 247 | } 248 | 249 | .drawflow-node-block-single, 250 | .drawflow-node-block-threshold { 251 | border-radius: 50%; 252 | background-color: lightcyan; 253 | } 254 | 255 | .drawflow-node-block span { 256 | display: inline-block; 257 | width: 100%; 258 | overflow: hidden; 259 | word-break: break-all; 260 | white-space: nowrap; 261 | text-overflow: ellipsis; 262 | } 263 | 264 | 265 | .drawflow-delete { 266 | position: absolute; 267 | top: -15px; 268 | right: -10px; 269 | display: block; 270 | width: 25px; 271 | height: 25px; 272 | border: 0; 273 | border-radius: 50%; 274 | line-height: 25px; 275 | font-weight: bold; 276 | text-align: center; 277 | outline: none; 278 | } 279 | 280 | .drawflow-modal-container { 281 | position: absolute; 282 | top: 0; 283 | left: 0; 284 | right: 0; 285 | bottom: 0; 286 | } 287 | 288 | .drawflow-modal-container:after { 289 | content: ""; 290 | display: block; 291 | position: absolute; 292 | top: 0; 293 | left: 0; 294 | right: 0; 295 | bottom: 0; 296 | background-color: rgba(0, 0, 0, 0.1); 297 | z-index: 1000; 298 | } 299 | 300 | .drawflow-modal-container > .drawflow-modal-content { 301 | display: inline-block; 302 | width: 500px; 303 | height: 400px; 304 | position: relative; 305 | top: 40%; 306 | left: 50%; 307 | transform: translate(-50%, -50%); 308 | z-index: 1001; 309 | margin: 0 auto; 310 | background-color: white; 311 | border-radius: 10px; 312 | } 313 | 314 | .drawflow-modal-content { 315 | padding: 15px; 316 | } 317 | 318 | .drawflow-modal-close { 319 | position: absolute; 320 | top: 10px; 321 | right: 10px; 322 | font-size: 20px; 323 | line-height: 20px; 324 | border: 1px solid black; 325 | background-color: lightgray; 326 | } 327 | 328 | .drawflow-modal-close:hover { 329 | background-color: gray; 330 | } 331 | 332 | .drawflow-modal-header { 333 | text-align: center; 334 | margin-bottom: 10px; 335 | } 336 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | , 9 | document.getElementById('root') 10 | ); 11 | 12 | // If you want to start measuring performance in your app, pass a function 13 | // to log results (for example: reportWebVitals(console.log)) 14 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 15 | reportWebVitals(); 16 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | --------------------------------------------------------------------------------