├── .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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------