├── .gitignore ├── LICENSE ├── README.en-US.md ├── README.md ├── example ├── index.html ├── index.jsx ├── index.less ├── mock_data │ └── data.js ├── package.json ├── tsconfig.json └── webpack.config.js ├── package.json ├── rollup.config.js ├── src ├── adaptor.js ├── canvas │ ├── canvas.js │ ├── edge.js │ ├── endpoint.js │ ├── group.js │ ├── node.js │ └── right-menu.js ├── index.d.ts ├── index.less ├── index.tsx ├── static │ ├── iconfont.css │ ├── iconfont.eot │ ├── iconfont.svg │ ├── iconfont.ttf │ ├── iconfont.woff │ └── iconfont.woff2 └── utils │ ├── getType.js │ └── layout.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | package-lock.json 5 | es 6 | pack 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alibaba Cloud 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.en-US.md: -------------------------------------------------------------------------------- 1 |

2 | A React-based Operations/Monitoring DAG Diagram 3 |

4 | 5 | English| [简体中文](. /README. md) 6 | 7 |

8 | 9 |

10 | 11 | ## ✨ Feature 12 | 13 | * support the direction of left-to-right, top-to-bottom 14 | * support for custom status, custom status note in upper left corner 15 | * support for custom node styles and hover, focus status 16 | * support edge's label style 17 | * support the toolltips of node, endpoint, edge's label 18 | * support right-click menu of nodes and edges 19 | * support minimap and highlight status 20 | * support edge flow animation 21 | 22 | ## 📦 Install 23 | 24 | ``` 25 | npm install react-monitor-dag 26 | ``` 27 | 28 | ## API: 29 | 30 | ### MonitorDag properties 31 | 32 | | Property | Description | Type | Default | 33 | |:-----------------:|:-----------------------------------------------------------------:|:---------------------------------------------------------------:|:-----------------------------------------------------------------------:| 34 | | data | data | any | - | 35 | | width | component width | number | string | - | 36 | | height | component height | number | string  | - | 37 | | className | component className | string | - | 38 | | nodeMenu | Node Right-click Menu Configuration | Array< [menu](#menu-type)> | (node) => Array< [menu](#menu-type)> | - | 39 | | nodeMenuClassName | Node Right-click Menu classname | string | - | 40 | | edgeMenu | Edge Right-click Menu Configuration | Array< [menu] 41 | | groupMenu | Group Right-click Menu Configuration | Array< [menu] 42 | (#menu-type)> | [ ] | 43 | | config | As configured above[config Prop](#config) | any | - | 44 | | polling | support polling[polling Prop](#polling) | object | { } | 45 | | registerStatus | Register status, which adds class to the node based on its status | object | key:value, registered by user, corresponded to the status field of node | 46 | | statusNote | Status note in upper left corner[statusNote Prop](#statusNote) | object | { } | 47 | | onClickNode | Single Click Node Event | (node) => void | - | 48 | | onClickCanvas | Click Canvas Event | () => void | - | 49 | | onContextmenuNode | Right-Click Node Event | (node) => void | - | 50 | | onDblClickNode | Double Click Node Event | (node) => void | - | 51 | | onClickEdge | Single Click Edge Event | (edge) => void | - | 52 | | onClickLabel | Single Click Label Event | (label, edge) => void | - | 53 | | onContextmenuEdge | Right-Click Edge Event | (edge) => void | - | 54 | | onContextmenuGroup | Right-Click Group Event | (data) => void | 55 | | onChangePage | Single-Click Group Pagination Event | (data) => void | 56 | | onNodeStatusChange | The canvas has a callback after the node state changes | (data) => void | 57 | 58 |
59 | ### menu 60 | 61 | right-click menu configuration for'Node/Edge' 62 | 63 | | Property | Description | Type | Default | 64 | |:--------:|:---------------------------------------:|:-----------------------------------------------:|:-------:| 65 | | title | name of each column | string | - | 66 | | key | unique flag for each column menu | string | - | 67 | | render | Customize the style of each column menu | (key) => void | - | 68 | | onClick | Click Callback for Each Column | (key, data) => void | - | 69 | 70 |
71 | 72 | ### config 73 | 74 | the configuration of canvas 75 | 76 | | Property | Description | Type | Default | 77 | |:------------------:|:--------------------------------:|:-------------------------------------------------------------:|:---------------------------------:| 78 | | direction | the dag's direction | string | `left-right` | `top-bottom` | 79 | | edge | the configuration of edge | [edge Prop](#edge-prop) { } | - | 80 | | labelRender | rendering method of edge's label | (label) => void | - | 81 | | labelTipsRender | rendering tooltips of edge label | (data) => void | - | 82 | | nodeRender | rendering of nodes | (data) => void | - | 83 | | nodeTipsRender | rendering tooltips of node | (data) => void | - | 84 | | endpointTipsRender | rendering tooltips of endpoint | (data) => void | - | 85 | | minimap | whether to show minimap | [minimap Prop](#minimap-prop) { } | - | 86 | | delayDraw | Delayed rendering. This component must ensure that the canvas container rendering (including animation execution) is completed before rendering, otherwise the coordinates will be offset, for example:Animation of Ant Design Modal | number | 0 | 87 | | autoLayout | custom layout | [autoLayout Prop](#auto-layout-prop) {} | - | 88 | | diffOptions | Collection of diff fields for node updates| Array< string> | - | 89 | | onLoaded | canvas loaded event| (data: {nodes, edges, groups}) => {} | - | 90 | 91 |
92 | 93 | ### edge 94 | 95 | the configuration of edge 96 | 97 | | Property | Description | Type | Default | 98 | |:--------:|:------------------:|:----------------------------------:|:-------:| 99 | | type | the type of edge | string | - | 100 | | config | the config of edge | any | - | 101 | 102 |
103 | 104 | ### group 105 | 106 | the configuration of group 107 | 108 | | Property | Description | Type | Default | 109 | |:------:|:--------:|:----------------------------------:|:-----:| 110 | | enableSearch | whether to enable the node group search node | boolean | false | 111 | | enablePagination | whether to turn on the page | boolean | true | 112 | | pageSize | nmber of per page | number | 20 | 113 | | rowCnt | the number of nodes are displayed in each row of the node group | number | 5 | 114 | 115 |
116 | 117 | ### minimap 118 | 119 | the configuration of minimap 120 | 121 | | Property | Description | Type | Default | 122 | |:--------:|:-----------------------:|:---------------------------------------------------------------------------:|:-------:| 123 | | enable | whether to show minimap | boolean | - | 124 | | config | the config of minimap | [minimap Config Prop](#minimap-config-prop) { } | - | 125 | 126 |
127 | 128 | ### autoLayout Config 129 | 130 | the custom layout config 131 | 132 | | Property | Description | Type | Default | 133 | |:--------:|:-----------------------:|:-------------------------------:|:-------:| 134 | | enable | whether to enable custom layout | boolean | - | 135 | | isAlways | whether to rearrange the layout after adding nodes | boolean | - | 136 | | config | algorithm configuration | { } | - | 137 | 138 |
139 | 140 | ### minimap Config 141 | 142 | the config of minimap 143 | 144 | | Property | Description | Type | Default | 145 | |:---------------:|:-----------------:|:-------------------------------:|:-------:| 146 | | nodeColor | node color | any | - | 147 | | activeNodeColor | node active color | any | - | 148 | 149 |
150 | 151 | ### polling 152 | 153 | support polling 154 | 155 | | Property | Description | Type | Default | 156 | |:--------:|:--------------------------:|:------------------------------------------:|:-------:| 157 | | enable | whether to support polling | boolean | - | 158 | | interval | interval of polling | number | - | 159 | | getData | the method of get data | (data) => void | - | 160 | 161 |
162 | 163 | ### statusNote 164 | 165 | Status note in upper left corner 166 | 167 | | Property | Description | Type | Default | 168 | |:--------:|:-------------------------------------------:|:---------------------------------------------------------:|:-------:| 169 | | enable | whether to show status in upper left corner | boolean | - | 170 | | notes | the configuration of status note | [notes Prop](#notes-prop) { } | - | 171 | 172 |
173 | 174 | ### notes 175 | 176 | the configuration of status note 177 | 178 | | Property | Description | Type | Default | 179 | |:---------:|:----------------:|:-----------------------------------:|:-------:| 180 | | code | status code | string | - | 181 | | className | status className | string | - | 182 | | text | status text | string | - | 183 | | render | custom rendering methods | function | - | 184 | 185 |
186 | 187 | ## 🔗API 188 | 189 | ``` jsx 190 | import MonitorDag from 'react-monitor-dag'; 191 | import 'react-monitor-dag/dist/index.css'; 192 | {}} // Single Click Node Event 198 | onContextmenuNode={(node) => {}} // Right Click Node Event 199 | onDblClickNode={(node) => {}} // Double Click Node Event 200 | onClickEdge={(edge) => {}} // Single Click Edge Event 201 | onContextmenuEdge={(edge) => {}} // Right Click Edge Event 202 | onContextmenuGroup={(data) => {}} // Right Click Group Event 203 | onChangePage={(data) => {}} // Single Click Group Pagination Event 204 | onNodeStatusChange={(data) => {}} // The canvas has a callback after the node state changes 205 | polling={{ // support polling 206 | enable: true, 207 | interval: 5000, // interval of polling 208 | getData: (data) => { // the method of get data 209 | 210 | } 211 | }} 212 | registerStatus={{ // Register status, which adds class to the node based on its status 213 | success: 'success-class', 214 | fail: 'fail-class', 215 | timeout: 'timeout-class', 216 | running: 'runnning-class', 217 | waitting: 'waiting-class', 218 | other: 'other-class' 219 | }} 220 | statusNote={{ // Status note in upper left corner 221 | enable: true, 222 | notes: [{ 223 | code: 'success', 224 | className: 'success-class', 225 | text: '运行成功', 226 | }] 227 | }} 228 | > 229 | 230 | ``` 231 | 232 | ``` js 233 | interface menu { // right-click menu configuration for'Node/Edge' 234 | title ? : string, // name of each column 235 | key: string, // unique flag for each column menu 236 | render ? (key: string) : void, // Customize the style of each column menu 237 | onClick ? (key: string, data: any) : void, // Click Callback for Each Column 238 | } 239 | 240 | interface config { 241 | direction: string, // the dag's direction: 'left-right' or 'top-bottom' 242 | edge: { // the configuration of edge 243 | type: string, 244 | config: any 245 | }, 246 | labelRender ? (label: JSX.Element) : void, // rendering method of edge's label 247 | labelTipsRender ? (data: any) : void, // rendering tooltips of edge label 248 | nodeRender ? (data: any) : void, // rendering of nodes 249 | nodeTipsRender ? (data: any) : void, // rendering tooltips of node 250 | endpointTipsRender ? (data: any) : void, // rendering tooltips of endpoint 251 | minimap: { // whether to show minimap 252 | enable: boolean, 253 | config: { 254 | nodeColor: any, // node color 255 | activeNodeColor: any // node active color 256 | } 257 | } 258 | } 259 | 260 | interface props { 261 | data: any, // data 262 | width ? : number | string, // component width 263 | height ? : number | string, // component height 264 | className ? : string, // component className 265 | nodeMenu: Array < menu > , // Node Right-click Menu Configuration 266 | edgeMenu: Array < menu > , // Edge Right-click Menu Configuration 267 | groupMenu: Array < menu > , // Group Right-click Menu Configuration 268 | config ? : any, // As configured above 269 | polling ? : { // support polling 270 | enable: boolean, 271 | interval: number, // interval of polling 272 | getData(data): void // the method of get data 273 | }, 274 | registerStatus ? : { // Register status, which adds class to the node based on its status 275 | success: string, 276 | fail: string 277 | }, 278 | statusNote ? : { // Status note in upper left corner 279 | enable: boolean, 280 | notes: [{ 281 | code: string, 282 | className: string, 283 | text: string, 284 | render?:() => JSX.Element 285 | }] 286 | }, 287 | onClickNode ? (node: any) : void, // Single Click Node Event 288 | onContextmenuNode ? (node: any) : void, // Right-Click Node Event 289 | onDblClickNode ? (node: any) : void, // Double Click Node Event 290 | onClickEdge ? (edge: any) : void, // Single Click Edge Event 291 | onClickLabel ? (label: string, edge: any) : void, // Single Click Label Event 292 | onContextmenuEdge ? (edge: any) : void, // Right-Click Edge Event 293 | onContextmenuGroup?(edge: any): void, // Right-Click Group Event 294 | onChangePage?(data:any): void, // Single-Click Group Pagination Event 295 | onNodeStatusChange?(data: any): void // The canvas has a callback after the node state changes 296 | } 297 | ``` 298 | 299 | If you need more customized requirements, you can refer to issue or [butterfly](https://github.com/alibaba/butterfly/blob/master/README.en-US.md) to customize your needs 300 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 一个基于React的运维/监控DAG图 3 |

4 | 5 | [English](./README.en-US.md) | 简体中文 6 | 7 |

8 | 9 |

10 | 11 | ## ✨ 特性 12 | 13 | * 支持左到右,上到下的布局 14 | * 支持自定义状态,左上角自定义状态注释 15 | * 支持自定义节点样式,以及hover,focus状态 16 | * 支持线段label样式 17 | * 支持节点,锚点,线段label的tooltips 18 | * 支持节点,线段的右键菜单 19 | * 支持minimap,以及高亮状态 20 | * 支持线段流动动画 21 | 22 | ## 📦 安装 23 | 24 | ``` 25 | npm install react-monitor-dag 26 | ``` 27 | 28 | ## API: 29 | 30 | ### MonitorDag属性 31 | 32 | | 参数 | 说明 | 类型 | 默认值 | 33 | |:-----------------:|:-----------------------------------------------:|:---------------------------------------------------------------:|:-------------------------------------------------:| 34 | | data | 画布数据 | any | - | 35 | | width | 组件宽度 | number | string | - | 36 | | height | 组件高度 | number | string  | - | 37 | | className | 组件类名 | string | - | 38 | | nodeMenu | 节点右键菜单配置 | Array< [menu](#menu-type)> | (node) => Array< [menu](#menu-type)> | - | 39 | | nodeMenuClassName | 节点右键菜单样式 | string | - | 40 | | edgeMenu | 线段右键菜单配置 | Array< [menu](#menu-type)> | [ ] | 41 | | config | 组件的画布配置,见[config Prop](#config) | any | - | 42 | | polling | 组件的轮训属性配置,见[polling Prop](#polling) | object | { } | 43 | | registerStatus | 自行注册状态,根据node的status给节点加上class | object | key:value的形式,可以自行注册,和node的status字段对应起来 | 44 | | statusNote | 画布左上角状态注释,见[statusNote Prop](#statusNote) | object | { } | 45 | | onClickNode | 单击节点事件 | (node) => void | - | 46 | | onClickCanvas | 单击画布事件 | () => void | - | 47 | | onContextmenuNode | 右键节点事件 | (node) => void | - | 48 | | onDblClickNode | 双击节点事件 | (node) => void | - | 49 | | onClickEdge | 单击线段事件 | (edge) => void | - | 50 | | onClickLabel | 单击label的事件 | (label, edge) => void | - | 51 | | onContextmenuEdge | 右键线段事件 | (edge) => void | - | 52 | | onContextmenuGroup | 右键group事件 | (edge) => void | - | 53 | | onChangePage | 单击group分页事件 | (edge) => void | - | 54 | | onNodeStatusChange | 画布有节点状态变化后的回调 | (data) => void | - | 55 | 56 |
57 | 58 | ### menu 59 | 60 | '节点/线段'的右键菜单配置 61 | 62 | | 参数 | 说明 | 类型 | 默认值 | 63 | |:-------:|:---------------------------:|:-----------------------------------------------:|:-----:| 64 | | title | 每列的展示的名字 | string | - | 65 | | key | 每列的唯一标志,对应数据上的key值 | string | - | 66 | | render | 支持每列的自定义样式 | (key) => void | - | 67 | | onClick | 每列的点击回调 | (key, data) => void | - | 68 | 69 |
70 | 71 | ### config 72 | 73 | 画布配置 74 | 75 | | 参数 | 说明 | 类型 | 默认值 | 76 | |:------------------:|:---------------------:|:-------------------------------------------------------------:|:---------------------------------:| 77 | | direction | 图的方向 | string | `left-right` | `top-bottom` | 78 | | edge | 定制线段的类型 | [edge Prop](#edge-prop) { } | - | 79 | | group | 定制节点组的类型 | [group Prop](#group-prop) { } | - | 80 | | labelRender | 线段label的渲染方法 | (label) => void | - | 81 | | labelTipsRender | 线段label tips的渲染方法 | (data) => void | - | 82 | | nodeRender | 节点的渲染方法 | (data) => void | - | 83 | | nodeTipsRender | 节点tips的渲染方法 | (data) => void | - | 84 | | endpointTipsRender | 锚点tips的渲染方法 | (data) => void | - | 85 | | minimap | 是否开启缩略图 | [minimap Prop](#minimap-prop) { } | - | 86 | | delayDraw | 是否延迟加载 | number | 0 | 87 | | autoLayout | 自定义布局 | [autoLayout Prop](#auto-layout-prop) {} | - | 88 | | diffOptions | 节点更新时diff的字段集合| Array< string> | - | 89 | | onLoaded | 画布渲染之后的回调| (data: {nodes, edges, groups}) => {} | - | 90 | 91 |
92 | 93 | ### edge 94 | 95 | 定制线段属性 96 | 97 | | 参数 | 说明 | 类型 | 默认值 | 98 | |:------:|:--------:|:----------------------------------:|:-----:| 99 | | type | 线段的类型 | string | - | 100 | | config | 线段的配置 | any | - | 101 | 102 |
103 | 104 | ### group 105 | 106 | 定制线段属性 107 | 108 | | 参数 | 说明 | 类型 | 默认值 | 109 | |:------:|:--------:|:----------------------------------:|:-----:| 110 | | enableSearch | 是否开启节点组搜索节点 | boolean | false | 111 | | enablePagination | 是否开启翻页 | boolean | true | 112 | | pageSize | 每页的数量 | number | 20 | 113 | | rowCnt | 节点组每行展示多少个节点 | number | 5 | 114 | 115 |
116 | 117 | ### minimap 118 | 119 | 缩略图属性 120 | 121 | | 参数 | 说明 | 类型 | 默认值 | 122 | |:------:|:-----------:|:---------------------------------------------------------------------------:|:-----:| 123 | | enable | 是否开启缩略图 | boolean | - | 124 | | config | 缩略图的配置 | [minimap Config Prop](#minimap-config-prop) { } | - | 125 | 126 |
127 | 128 | 129 | ### minimap Config 130 | 131 | 缩略图的配置 132 | 133 | | 参数 | 说明 | 类型 | 默认值 | 134 | |:---------------:|:----------:|:-------------------------------:|:-----:| 135 | | nodeColor | 节点颜色 | any | - | 136 | | activeNodeColor | 节点激活颜色 | any | - | 137 | 138 |
139 | 140 | ### autoLayout Config 141 | 142 | 自动布局的配置 143 | 144 | | 参数 | 说明 | 类型 | 默认值 | 145 | |:---------------:|:----------:|:-------------------------------:|:-----:| 146 | | enable | 是否开启自动布局 | boolean | - | 147 | | isAlways | 否添加节点后就重新布局 | boolean | - | 148 | | config | 算法配置 | { } | - | 149 | 150 |
151 | 152 | ### polling 153 | 154 | 轮训属性配置 155 | 156 | | 参数 | 说明 | 类型 | 默认值 | 157 | |:--------:|:----------:|:------------------------------------------:|:-----:| 158 | | enable | 是否支持轮训 | boolean | - | 159 | | interval | 轮训时间 | number | - | 160 | | getData | 轮训方法 | (data) => void | - | 161 | 162 |
163 | 164 | ### statusNote 165 | 166 | 画布左上角状态配置 167 | 168 | | 参数 | 说明 | 类型 | 默认值 | 169 | |:------:|:------------------:|:---------------------------------------------------------:|:-----:| 170 | | enable | 是否开启左上角状态显示 | boolean | - | 171 | | notes | 左上角状态配置信息 | [notes Prop](#notes-prop) { } | - | 172 | 173 |
174 | 175 | ### notes 176 | 177 | 左上角状态配置信息 178 | 179 | | 参数 | 说明 | 类型 | 默认值 | 180 | |:---------:|:---------------:|:-----------------------------------:|:-----:| 181 | | code | 左上角状态 | string | - | 182 | | className | 左上角状态栏类名 | string | - | 183 | | text | 左上角状态显示文字 | string | - | 184 | | render | 自定义渲染方法 | function | - | 185 | 186 |
187 | 188 | ## 🔗API 189 | 190 | ``` jsx 191 | import MonitorDag from 'react-monitor-dag'; 192 | import 'react-monitor-dag/dist/index.css'; 193 | {}} // Single Click Node Event 198 | onContextmenuNode={(node) => {}} // Right Click Node Event 199 | onDblClickNode={(node) => {}} // Double Click Node Event 200 | onClickEdge={(edge) => {}} // Single Click Edge Event 201 | onContextmenuEdge={(edge) => {}} // Right Click Edge Event 202 | onContextmenuGroup={(data) => {}} // Right Click Group Event 203 | onChangePage={(data) => {}} // Single Click Group Pagination Event 204 | onNodeStatusChange={(data) => {}} // the canvas has a callback after the node state changes 205 | polling={{ // support polling 206 | enable: true, 207 | interval: 5000, // interval of polling 208 | getData: (data) => { // the method of get data 209 | 210 | } 211 | }} 212 | registerStatus={{ // Register status, which adds class to the node based on its status 213 | success: 'success-class', 214 | fail: 'fail-class', 215 | timeout: 'timeout-class', 216 | running: 'runnning-class', 217 | waitting: 'waiting-class', 218 | other: 'other-class' 219 | }} 220 | statusNote={{ // Status note in upper left corner 221 | enable: true, 222 | notes: [{ 223 | code: 'success', 224 | className: 'success-class', 225 | text: '运行成功' 226 | }] 227 | }} 228 | > 229 | 230 | ``` 231 | 232 | ``` javascript 233 | interface menu { // '节点/线段'的右键菜单配置 234 | title ? : string, // 每列的展示的名字 235 | key: string, // 每列的唯一标志,对应数据上的key值 236 | render ? (key: string) : void, // 支持每列的自定义样式 237 | onClick ? (key: string, data: any) : void, // 每列的点击回调 238 | } 239 | 240 | interface config { 241 | direction: string, // 图的方向: 'left-right' or 'top-bottom' 242 | edge: { // 定制线段的类型 243 | type: string, 244 | config: any 245 | }, 246 | labelRender ? (label: JSX.Element) : void, // 线段label的渲染方法 247 | labelTipsRender ? (data: any) : void, // 线段label tips的渲染方法 248 | nodeRender ? (data: any) : void, // 节点的渲染方法 249 | nodeTipsRender ? (data: any) : void, // 节点tips的渲染方法 250 | endpointTipsRender ? (data: any) : void, // 锚点tips的渲染方法 251 | minimap: { // 是否开启缩略图 252 | enable: boolean, 253 | config: { 254 | nodeColor: any, // 节点颜色 255 | activeNodeColor: any // 节点激活颜色 256 | } 257 | } 258 | } 259 | 260 | interface props { 261 | data: any, // 画布数据 262 | width ? : number | string, // 组件宽 263 | height ? : number | string, // 组件高 264 | className ? : string, // 组件classname 265 | nodeMenu: Array < menu > , // 节点右键菜单配置 266 | edgeMenu: Array < menu > , // 线段右键菜单配置 267 | config ? : any, // 画布配置 268 | polling ? : { // 支持轮训 269 | enable: boolean, 270 | interval: number, // 轮训时间 271 | getData(data): void // 轮训方法 272 | }, 273 | registerStatus ? : { // 自行注册状态,会根据node的status给节点加上class 274 | success: string, 275 | fail: string, 276 | // key:value的形式,可以自行注册,和node的status字段对应起来 277 | }, 278 | statusNote ? : { // 画布左上角状态注释 279 | enable: boolean, 280 | notes: [{ 281 | code: string, 282 | className: string, 283 | text: string, 284 | render?: () => JSX.Element 285 | }] 286 | }, 287 | onClickNode ? (node: any) : void, // 单击节点事件 288 | onContextmenuNode ? (node: any) : void, // 右键节点事件 289 | onDblClickNode ? (node: any) : void, // 双击节点事件 290 | onClickEdge ? (edge: any) : void, // 单击线段事件 291 | onClickLabel ? (label: string, edge: any) : void, //单击label的事件 292 | onContextmenuEdge ? (edge: any) : void, // 右键线段事件 293 | onContextmenuGroup?(edge: any): void, // 右键group事件 294 | onChangePage?(data:any): void, // 单击分页事件&搜索 295 | onNodeStatusChange?(data: any): void // 画布有节点状态变化后的回调 296 | } 297 | ``` 298 | 299 | 如需要更多定制的需求,您可以提issue或者参考[Butterfly](https://github.com/alibaba/butterfly)来定制您需要的需求 300 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | DTDesign-React运维/监控图 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /example/index.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, { useState, useEffect } from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { BrowserRouter as Router } from 'react-router-dom'; 6 | import { Layout, Pagination, Input } from 'antd'; 7 | import { CloseCircleOutlined, CheckOutlined } from '@ant-design/icons'; 8 | import MonitorDag from '../src/index.tsx'; 9 | import mockData from './mock_data/data'; 10 | import _ from 'lodash'; 11 | import 'antd/dist/antd.css'; 12 | import './index.less'; 13 | 14 | const { Header } = Layout; 15 | const { Search } = Input; 16 | 17 | const edgeMenu = [{ 18 | key: 'detail', 19 | title: '线段信息', 20 | onClick: (key, data) => { 21 | console.log('click detail info', data) 22 | } 23 | }, { 24 | key: '监控流程', 25 | render: (key, data) => { 26 | return 监控流程 27 | }, 28 | onClick: (key, data) => { 29 | console.log('monitor edge', data); 30 | } 31 | }] 32 | 33 | const groupMenu = [{ 34 | key: 'detail', 35 | title: '节点组信息', 36 | onClick: (key, data) => { 37 | console.log('click detail info') 38 | } 39 | }, { 40 | key: 'run', 41 | render: (key, data) => { 42 | return 节点运行 43 | }, 44 | onClick: (key, data) => { 45 | console.log('run node'); 46 | } 47 | }] 48 | const Demo = () => { 49 | const [canvasData, setCanvasData] = useState(mockData); 50 | const nodeMenu = [{ 51 | key: 'detail', 52 | title: '节点信息', 53 | onClick: (key, data) => { 54 | console.log('click detail info'); 55 | } 56 | }, { 57 | key: 'run', 58 | render: (key, data) => { 59 | return 节点运行 60 | }, 61 | onClick: (key, data) => { 62 | console.log('run node'); 63 | } 64 | }]; 65 | return { 78 | return label; 79 | }, 80 | labelTipsRender: (label, info) => { 81 | return `${label}: 自定义label tips`; 82 | }, 83 | nodeRender: (nodeOpts) => { 84 | return ( 85 | {nodeOpts.title + nodeOpts.id + nodeOpts.status} 86 | ) 87 | }, 88 | // diffOptions: ['status'], 89 | // statusNote: { 90 | // notes: [{ 91 | // code: 'fail', 92 | // render: () => { 93 | // return 失败 94 | // } 95 | // }, { 96 | // code: 'success', 97 | // render: () => { 98 | // return 成功 99 | // } 100 | // }] 101 | // }, 102 | nodeTipsRender: (nodeOpts) => { 103 | return {nodeOpts.title}: 自定义节点tips 104 | }, 105 | endpointTipsRender: (pointOpts) => { 106 | return 自定义锚点tips 107 | }, 108 | // onSearchGroup: (keywork, nodeList) => { 109 | // console.log(keywork); 110 | // console.log(nodeList); 111 | // return nodeList; 112 | // }, 113 | group: { 114 | enableSearch: true, 115 | enablePagination: true, 116 | pageSize: 10 117 | }, 118 | minimap: { 119 | enable: true, 120 | config: { 121 | nodeColor: 'rgba(216, 216, 216, 0.13)', 122 | activeNodeColor: '#F66902', 123 | viewportStyle: { 124 | 'background-color': 'rgba(216, 216, 216, 0.07)' 125 | }, 126 | groups: mockData.groups, 127 | nodes: mockData.nodes 128 | } 129 | }, 130 | }} 131 | /> 132 | } 133 | 134 | ReactDOM.render(( 135 | 136 | 137 |
DTDesign-React运维/监控图
138 | 139 | 140 | 141 |
142 |
143 | ), document.getElementById('main')); 144 | -------------------------------------------------------------------------------- /example/index.less: -------------------------------------------------------------------------------- 1 | html, body { 2 | padding: 0; 3 | margin: 0; 4 | width: 100%; 5 | height: 100%; 6 | } 7 | 8 | #main { 9 | width: 100%; 10 | height: 100%; 11 | .ant-layout, .ant-layout-content { 12 | height: 100%; 13 | } 14 | 15 | .menu { 16 | height: 100%; 17 | width: 200px; 18 | overflow-y: auto; 19 | } 20 | 21 | .header.ant-layout-header { 22 | background-color: #212528; 23 | color: #fff; 24 | height: 50px; 25 | line-height: 50px; 26 | } 27 | 28 | section.ant-layout { 29 | background: #2E2E2E; 30 | } 31 | 32 | .butterfly-monitor-dag { 33 | width: 100%; 34 | height: 100%; 35 | .schedule-node { 36 | .node-text { 37 | margin-left: 6px; 38 | } 39 | } 40 | } 41 | } 42 | .view-port-background { 43 | background-color: rgba(216, 216, 216, 0.05); 44 | } 45 | 46 | 47 | -------------------------------------------------------------------------------- /example/mock_data/data.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | export default { 3 | nodes: [ 4 | { 5 | id: '1', 6 | top: 50, 7 | left: 500, 8 | title: '任务1', 9 | className: 'icon-background-color', 10 | iconType: 'icon-kaifa', 11 | status: 'success' 12 | }, { 13 | id: '2', 14 | top: 150, 15 | left: 500, 16 | title: '任务2', 17 | className: 'icon-background-color', 18 | iconType: 'icon-kaifa', 19 | status: 'running' 20 | }, { 21 | id: '3', 22 | top: 250, 23 | left: 500, 24 | title: '任务3', 25 | className: 'icon-background-color', 26 | iconType: 'icon-kaifa', 27 | status: 'waiting' 28 | }, { 29 | id: '4', 30 | top: 350, 31 | left: 500, 32 | title: '任务4', 33 | className: 'icon-background-color', 34 | iconType: 'icon-kaifa', 35 | status: 'timeout' 36 | }, { 37 | id:'5', 38 | top: 450, 39 | left: 500, 40 | title: '任务5', 41 | className: 'icon-background-color', 42 | iconType: 'icon-kaifa', 43 | status: 'fail' 44 | }, { 45 | id: '6', 46 | top: 550, 47 | left: 500, 48 | title: '自定义', 49 | className: 'icon-background-color', 50 | iconType: 'icon-kaifa', 51 | status: 'other', 52 | endpoints: [{ 53 | id: 'top', 54 | orientation: [0, -1], 55 | pos: [0.5, 0] 56 | },{ 57 | id: 'bottom', 58 | orientation: [0, 1], 59 | pos: [0.5, 0] 60 | }] 61 | }, 62 | { 63 | id: '21', 64 | top: 550, 65 | left: 500, 66 | title: '自定义21', 67 | className: 'icon-background-color', 68 | iconType: 'icon-kaifa', 69 | status: 'other', 70 | endpoints: [{ 71 | id: 'top', 72 | orientation: [0, -1], 73 | pos: [0.5, 0] 74 | },{ 75 | id: 'bottom', 76 | orientation: [0, 1], 77 | pos: [0.5, 0] 78 | }] 79 | }, 80 | { 81 | id: '22', 82 | top: 550, 83 | left: 500, 84 | title: '自定义21', 85 | className: 'icon-background-color', 86 | iconType: 'icon-kaifa', 87 | status: 'other', 88 | endpoints: [{ 89 | id: 'top', 90 | orientation: [0, -1], 91 | pos: [0.5, 0] 92 | },{ 93 | id: 'bottom', 94 | orientation: [0, 1], 95 | pos: [0.5, 0] 96 | }] 97 | }, 98 | { 99 | id: '7', 100 | title: '某某算法11', 101 | className: 'icon-background-color', 102 | iconType: 'icon-kaifa', 103 | top: 55, 104 | left: 15, 105 | group: 'group', 106 | status: 'success', 107 | }, 108 | { 109 | id: '8', 110 | title: '某某算法22', 111 | className: 'icon-background-color', 112 | iconType: 'icon-kaifa', 113 | top: 55, 114 | left: 130, 115 | group: 'group', 116 | status: 'success', 117 | }, 118 | { 119 | id: '9', 120 | title: '某某算法33', 121 | className: 'icon-background-color', 122 | iconType: 'icon-kaifa', 123 | top: 55, 124 | left: 245, 125 | group: 'group', 126 | status: 'success', 127 | }, 128 | { 129 | id: '10', 130 | title: '某某算法44', 131 | className: 'icon-background-color', 132 | iconType: 'icon-kaifa', 133 | top: 55, 134 | left: 360, 135 | group: 'group', 136 | status: 'success', 137 | }, 138 | { 139 | id: '11', 140 | title: '某某算法55', 141 | className: 'icon-background-color', 142 | iconType: 'icon-kaifa', 143 | top: 110, 144 | left: 15, 145 | group: 'group', 146 | status: 'success', 147 | }, 148 | { 149 | id: '12', 150 | title: '某某算法66', 151 | className: 'icon-background-color', 152 | iconType: 'icon-kaifa', 153 | top: 110, 154 | left: 130, 155 | group: 'group', 156 | status: 'success', 157 | }, 158 | { 159 | id: '13', 160 | title: '某某算法77', 161 | className: 'icon-background-color', 162 | iconType: 'icon-kaifa', 163 | top: 110, 164 | left: 245, 165 | group: 'group', 166 | status: 'success', 167 | }, 168 | { 169 | id: '14', 170 | title: '某某算法88', 171 | className: 'icon-background-color', 172 | iconType: 'icon-kaifa', 173 | top: 110, 174 | left: 360, 175 | group: 'group', 176 | status: 'success', 177 | }, 178 | { 179 | id: '15', 180 | title: '某某算法99', 181 | className: 'icon-background-color', 182 | iconType: 'icon-kaifa', 183 | top: 165, 184 | left: 15, 185 | group: 'group', 186 | status: 'success', 187 | }, 188 | { 189 | id: '16', 190 | title: '某某算法00', 191 | className: 'icon-background-color', 192 | iconType: 'icon-kaifa', 193 | top: 165, 194 | left: 130, 195 | group: 'group', 196 | status: 'success', 197 | }, 198 | { 199 | id: '17', 200 | title: '某某算法qq', 201 | className: 'icon-background-color', 202 | iconType: 'icon-kaifa', 203 | top: 165, 204 | left: 245, 205 | group: 'group', 206 | status: 'success', 207 | }, 208 | { 209 | id: '18', 210 | title: '某某算法ww', 211 | className: 'icon-background-color', 212 | iconType: 'icon-kaifa', 213 | top: 165, 214 | left: 360, 215 | group: 'group', 216 | status: 'success', 217 | }, 218 | { 219 | id: '19', 220 | title: '某某算法ee', 221 | className: 'icon-background-color', 222 | iconType: 'icon-kaifa', 223 | top: 220, 224 | left: 15, 225 | group: 'group', 226 | status: 'success', 227 | }, 228 | { 229 | id: '20', 230 | title: '交运算rr', 231 | className: 'icon-background-color', 232 | iconType: 'icon-guanlian', 233 | top: 220, 234 | left: 130, 235 | group: 'group', 236 | status: 'success', 237 | }, 238 | ], 239 | edges: [ 240 | { 241 | source: '1', 242 | target: '2', 243 | sourceNode: '1', 244 | targetNode: '2', 245 | arrow: true, 246 | type: 'endpoint', 247 | arrowPosition: 0.5, 248 | }, 249 | { 250 | source: '2', 251 | target: '3', 252 | sourceNode: '2', 253 | targetNode: '3', 254 | arrow: true, 255 | flow: true, 256 | type: 'endpoint', 257 | arrowPosition: 0.5, 258 | }, 259 | { 260 | source: '3', 261 | target: '4', 262 | sourceNode: '3', 263 | targetNode: '4', 264 | arrow: true, 265 | type: 'endpoint', 266 | arrowPosition: 0.5, 267 | }, 268 | { 269 | source: '4', 270 | target: '5', 271 | sourceNode: '4', 272 | targetNode: '5', 273 | arrow: true, 274 | type: 'endpoint', 275 | arrowPosition: 0.5, 276 | }, 277 | { 278 | source: '5', 279 | target: '6', 280 | sourceNode: '5', 281 | targetNode: '6', 282 | arrow: true, 283 | type: 'endpoint', 284 | label: 'test label', 285 | arrowPosition: 0.5, 286 | }, 287 | { 288 | source: 'bottom', 289 | target: 'groupTop', 290 | sourceNode: '6', 291 | targetNode: 'group', 292 | arrow: true, 293 | type: 'endpoint', 294 | arrowPosition: 0.5, 295 | }, 296 | { 297 | source: '5', 298 | target: '21', 299 | sourceNode: '5', 300 | targetNode: '21', 301 | arrow: true, 302 | type: 'endpoint', 303 | arrowPosition: 0.5, 304 | }, 305 | { 306 | source: 'groupBottom', 307 | target: 'top', 308 | sourceNode: 'group', 309 | targetNode: '22', 310 | arrow: true, 311 | type: 'endpoint', 312 | arrowPosition: 0.5, 313 | } 314 | ], 315 | groups: [{ 316 | id: 'group', 317 | options: { 318 | title: '测试' 319 | }, 320 | top: 650, 321 | left: 300, 322 | width: 820, 323 | height: 300, 324 | resize: true, 325 | // draggable: false, 326 | endpoints: [{ 327 | id: 'groupTop', 328 | orientation: [0, -1], 329 | pos: [0.5, 0], 330 | },{ 331 | id: 'groupBottom', 332 | orientation: [0, 1], 333 | pos: [0.5, 0], 334 | }] 335 | }], 336 | }; 337 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "webpack-dev-server" 9 | }, 10 | "author": "jambin", 11 | "license": "MIT", 12 | "dependencies": { 13 | "antd": "~4.10.3", 14 | "jquery": "^3.4.1", 15 | "react": "~16.12.0", 16 | "react-dom": "^16.13.1", 17 | "react-router": "~5.1.2", 18 | "react-router-dom": "~5.1.2" 19 | }, 20 | "devDependencies": { 21 | "@ant-design/icons": "^4.6.2", 22 | "@babel/core": "~7.8.3", 23 | "@babel/plugin-proposal-class-properties": "~7.8.3", 24 | "@babel/plugin-proposal-object-rest-spread": "~7.8.3", 25 | "@babel/plugin-transform-runtime": "^7.12.1", 26 | "@babel/preset-env": "~7.8.3", 27 | "@babel/preset-react": "~7.8.3", 28 | "babel-loader": "8.0.6", 29 | "babel-plugin-transform-es2015-modules-commonjs": "~6.26.2", 30 | "css-loader": "~1.0.0", 31 | "eslint": "~5.16.0", 32 | "eslint-config-aliyun": "~2.0.3", 33 | "eslint-plugin-react": "~7.13.0", 34 | "file-loader": "~2.0.0", 35 | "html-webpack-plugin": "^3.2.0", 36 | "less": "~3.7.0", 37 | "less-loader": "~4.1.0", 38 | "lodash": "^4.17.21", 39 | "mini-css-extract-plugin": "~0.9.0", 40 | "style-loader": "~0.21.0", 41 | "ts-loader": "~8.0.14", 42 | "url-loader": "~1.0.1", 43 | "webpack": "~4.41.5", 44 | "webpack-cli": "~3.0.8", 45 | "webpack-dev-server": "~3.10.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": false, 5 | "module": "commonjs", 6 | "target": "es5", 7 | "jsx": "react", 8 | "allowJs": true, 9 | "esModuleInterop": true 10 | } 11 | } -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | const path = require('path'); 6 | 7 | module.exports = { 8 | entry: { 9 | app: './index.jsx' 10 | }, 11 | output: { 12 | filename: '[name].js', 13 | chunkFilename: '[name].js' 14 | }, 15 | resolve: { 16 | modules: [ 17 | path.resolve(process.cwd(), 'node_modules'), 18 | path.resolve(process.cwd(), '../node_modules'), 19 | 'node_modules' 20 | ], 21 | extensions: ['.js', '.jsx'] 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.tsx?$/, 27 | use: 'ts-loader', 28 | exclude: /node_modules/, 29 | }, 30 | { 31 | test: /\.(js|jsx)$/, 32 | exclude: /(node_modules|bower_components)/, 33 | use: { 34 | loader: 'babel-loader', 35 | options: { 36 | presets: [ 37 | '@babel/preset-env', 38 | '@babel/preset-react' 39 | ], 40 | plugins: [ 41 | '@babel/plugin-transform-runtime', 42 | '@babel/plugin-transform-modules-commonjs', 43 | '@babel/plugin-proposal-object-rest-spread', 44 | '@babel/plugin-proposal-class-properties', 45 | ] 46 | } 47 | } 48 | }, 49 | { 50 | test: /\.(woff|woff2|eot|ttf|otf)$/, 51 | use: { 52 | loader: 'file-loader', 53 | 54 | options: { 55 | name: '[name][hash].[ext]', 56 | outputPath: 'fonts/' 57 | } 58 | } 59 | }, 60 | { 61 | test: /\.(less|css)$/, 62 | use: [ 63 | { 64 | loader: 'style-loader' 65 | }, 66 | { 67 | loader: 'css-loader' 68 | }, 69 | { 70 | loader: 'less-loader', 71 | options: { 72 | javascriptEnabled: true 73 | } 74 | } 75 | ] 76 | }, 77 | { 78 | test: /\.(png|jpg|gif|svg)$/, 79 | use: [ 80 | { 81 | loader: 'url-loader' 82 | } 83 | ] 84 | } 85 | ] 86 | }, 87 | plugins: [ 88 | new MiniCssExtractPlugin({ 89 | filename: '[name].css' 90 | }), 91 | new HtmlWebpackPlugin({ 92 | template: './index.html' 93 | }) 94 | ], 95 | devServer: { 96 | contentBase: './dist', // 本地服务器所加载的页面所在的目录 97 | historyApiFallback: true, // 不跳转 98 | inline: true, // 实时刷新 99 | index: 'index.html', 100 | port: 8080, 101 | open: true 102 | } 103 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-monitor-dag", 3 | "version": "1.2.3", 4 | "description": "一个基于React的运维/监控DAG图", 5 | "main": "dist/index.js", 6 | "pack": "pack/index.js", 7 | "directories": { 8 | "example": "example" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "build": "rollup -c && cp -r ./src/static ./dist && cp -r ./src/static ./pack", 13 | "dev": "rollup -w -c" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git@gitlab.alibaba-inc.com:DataQ-FE-Components/butterfly-monitor-dag.git" 18 | }, 19 | "author": "无惟", 20 | "license": "MIT", 21 | "dependencies": { 22 | 23 | }, 24 | "peerDependencies": { 25 | "react": ">15.6.1", 26 | "react-dom": ">15.6.1", 27 | "lodash": "^4.17.20", 28 | "jquery": ">=2.0.0", 29 | "butterfly-dag": ">=4.3.0" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.12.17", 33 | "@babel/plugin-proposal-class-properties": "^7.12.13", 34 | "@babel/plugin-proposal-object-rest-spread": "^7.12.13", 35 | "@babel/plugin-transform-modules-commonjs": "^7.13.8", 36 | "@babel/plugin-transform-runtime": "^7.13.10", 37 | "@babel/preset-env": "^7.12.17", 38 | "@babel/preset-react": "^7.12.13", 39 | "@babel/preset-typescript": "^7.12.17", 40 | "@types/lodash": "^4.14.168", 41 | "@types/react": "^17.0.3", 42 | "@types/react-dom": "^17.0.3", 43 | "babel-loader": "^8.2.2", 44 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", 45 | "butterfly-dag": "~4.3.0", 46 | "less": "^3.12.2", 47 | "postcss": "^8.1.14", 48 | "rollup": "^2.38.5", 49 | "rollup-plugin-babel": "^4.4.0", 50 | "rollup-plugin-commonjs": "^10.1.0", 51 | "rollup-plugin-extensions": "^0.1.0", 52 | "rollup-plugin-json": "^4.0.0", 53 | "rollup-plugin-peer-deps-external": "^2.2.4", 54 | "rollup-plugin-postcss": "^4.0.0", 55 | "rollup-plugin-typescript2": "^0.29.0", 56 | "rollup-plugin-url": "^3.0.1", 57 | "typescript": "~4.6.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import json from 'rollup-plugin-json'; 3 | import babel from 'rollup-plugin-babel'; 4 | import postcss from 'rollup-plugin-postcss'; 5 | import commonjs from 'rollup-plugin-commonjs'; 6 | import extensions from 'rollup-plugin-extensions'; 7 | import external from 'rollup-plugin-peer-deps-external'; 8 | import url from "rollup-plugin-url"; 9 | import typescript from 'rollup-plugin-typescript2'; 10 | 11 | import pkg from './package.json'; 12 | 13 | const config = { 14 | presets: [ 15 | '@babel/preset-typescript', 16 | '@babel/preset-react', 17 | '@babel/preset-env' 18 | ], 19 | plugins: [ 20 | '@babel/plugin-proposal-class-properties' 21 | ] 22 | }; 23 | 24 | 25 | const plugins = [ 26 | extensions({ 27 | extensions: ['.js'], 28 | resolveIndex: true, 29 | }), 30 | external(), 31 | babel(Object.assign({ 32 | exclude: [ 33 | 'node_modules/**', 34 | ] 35 | }, config)), 36 | postcss({ 37 | extract: true, 38 | modules: false, 39 | use: [ 40 | [ 41 | 'less', 42 | { 43 | javascriptEnabled: true, 44 | } 45 | ] 46 | ] 47 | }), 48 | commonjs(), 49 | json(), 50 | url({ 51 | limit: 100 * 1024, // inline files < 100k, copy files > 100k 52 | include: ["**/*.svg", "**/*.eot", "**/*.tff", "**/*.woff", "**/*.woff2"], // defaults to .svg, .png, .jpg and .gif files 53 | emitFiles: true // defaults to true 54 | }), 55 | typescript({ 56 | tsconfigOverride: { 57 | compilerOptions: { 58 | module: "es2015" 59 | } 60 | } 61 | }) 62 | ]; 63 | 64 | const rollupCfg = []; 65 | 66 | // all in one 构建 67 | rollupCfg.push({ 68 | input: path.join(__dirname, 'src/index.tsx'), 69 | output: [ 70 | { 71 | file: pkg.pack, 72 | format: 'cjs', 73 | exports: 'named', 74 | sourcemap: 'inline' 75 | }, 76 | { 77 | file: pkg.main, 78 | format: 'es', 79 | sourcemap: 'inline' 80 | } 81 | ], 82 | plugins 83 | }); 84 | 85 | export default rollupCfg; 86 | -------------------------------------------------------------------------------- /src/adaptor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import ScheduleNode from './canvas/node'; 4 | import Edge from './canvas/edge'; 5 | import Group from './canvas/group'; 6 | import * as _ from 'lodash'; 7 | 8 | // const nodePositionFn = (data, pageData) => { 9 | // let row = Math.floor(data.width / 125 ); 10 | // let cow = Math.floor(data.height / 40 ); 11 | // return pageData.map((item,index) => { 12 | // if(index < row) { 13 | // item.top = 55 14 | // item.left = 125 * (index) + 12 15 | // } 16 | // for(let i = 2; i <= cow; i++) { 17 | // if(index >= (i - 1) * row && index < i * row ) { 18 | // item.top = 55 * i 19 | // item.left = 125 * (index - (i - 1) * row) + 12 20 | // } 21 | // } 22 | // return item 23 | // }) 24 | // }; 25 | 26 | 27 | export let transformInitData = (info) => { 28 | let { 29 | data, config, nodeMenu, 30 | edgeMenu,groupMenu, registerStatus, 31 | nodeMenuClassName, groupCfg = {} 32 | } = info; 33 | 34 | let nodes = (data.nodes || []).map((item) => { 35 | return _.assign(item, { 36 | _config: config, 37 | _menu: nodeMenu, 38 | _registerStatus: registerStatus, 39 | _nodeMenuClassName: nodeMenuClassName, 40 | Class: ScheduleNode 41 | }); 42 | }); 43 | 44 | let edges = (data.edges || []).map((item) => { 45 | return _.assign(item, { 46 | id: `${item.source}-${item.target}`, 47 | type: 'endpoint', 48 | sourceNode: item.sourceNode, 49 | targetNode: item.targetNode, 50 | source: `${item.source}`, 51 | target: `${item.target}`, 52 | _config: config, 53 | _menu: edgeMenu, 54 | Class: Edge 55 | }); 56 | }); 57 | 58 | let groups = (data.groups || []).map((item) => { 59 | return _.assign(item, { 60 | options: { 61 | ...item.options, 62 | _menu: groupMenu, 63 | }, 64 | enableSearch: _.get(config, 'group.enableSearch', false), 65 | Class: Group, 66 | }); 67 | }); 68 | 69 | 70 | // group的分页处理 71 | let _groupEnablePagination = groupCfg.enablePagination; 72 | let _groupPageSize = groupCfg.pageSize || 20; 73 | if (_groupEnablePagination && groups && groups.length > 0) { 74 | 75 | let _groupsObj = {}; 76 | let _groupHiddenNodes = {}; 77 | 78 | for(let i = 0; i < groups.length; i++) { 79 | let _group = groups[i]; 80 | if (!_groupsObj[_group.id]) { 81 | _groupsObj[_group.id] = []; 82 | } 83 | } 84 | for(let i = 0; i < nodes.length; i++) { 85 | let _node = nodes[i]; 86 | if (_node.group) { 87 | _groupsObj[_node.group].push(_node); 88 | } 89 | } 90 | 91 | groups.forEach((item) => { 92 | let _nodes = _groupsObj[item.id]; 93 | item._enablePagination = _groupEnablePagination; 94 | item._allNodeList = item._showNodeList = _nodes; 95 | item._pageSize = _groupPageSize; 96 | item._pageNum = 1; 97 | item._totalNum = _nodes.length; 98 | }); 99 | 100 | let _rmNodes = []; 101 | for(let key in _groupsObj) { 102 | let _group = _groupsObj[key]; 103 | _rmNodes = _rmNodes.concat(_group.slice(_groupPageSize, _group.length)); 104 | } 105 | 106 | nodes = nodes.filter((item) => { 107 | let isRmNode = _.some(_rmNodes, (node) => node.id === item.id); 108 | if (isRmNode) { 109 | _groupHiddenNodes[item.id] = item; 110 | } 111 | return !isRmNode; 112 | }); 113 | } 114 | 115 | return { 116 | nodes, 117 | edges, 118 | groups 119 | } 120 | } 121 | 122 | export let diffPropsData = (newData, oldData, diffOptions = []) => { 123 | let updateNodes = []; 124 | let addNodes = _.differenceWith(newData.nodes, oldData.nodes, (a, b) => { 125 | return a.id === b.id; 126 | }); 127 | let rmNodes = _.differenceWith(oldData.nodes, newData.nodes, (a, b) => { 128 | return a.id === b.id; 129 | }); 130 | let addGroups = _.differenceWith(newData.groups, oldData.groups, (a, b) => { 131 | return a.id === b.id; 132 | }); 133 | let rmGroups = _.differenceWith(oldData.groups, newData.groups, (a, b) => { 134 | return a.id === b.id; 135 | }); 136 | 137 | if (diffOptions.length > 0) { 138 | updateNodes = _.differenceWith(newData.nodes, oldData.nodes, (a, b) => { 139 | return diffOptions.reduce((pre, cur) => { 140 | return pre && a[cur] === b[cur]; 141 | }, a[diffOptions[0]] === b[diffOptions[0]]); 142 | }) 143 | } 144 | let addEdges = _.differenceWith(newData.edges, oldData.edges, (a, b) => { 145 | return ( 146 | a.sourceNode === b.sourceNode && 147 | a.targetNode === b.targetNode && 148 | a.sourceEndpoint === b.sourceEndpoint && 149 | a.targetEndpoint === b.targetEndpoint 150 | ); 151 | }); 152 | let rmEdges = _.differenceWith(oldData.edges, newData.edges, (a, b) => { 153 | return ( 154 | a.sourceNode === b.sourceNode && 155 | a.targetNode === b.targetNode && 156 | a.sourceEndpoint === b.sourceEndpoint && 157 | a.targetEndpoint === b.targetEndpoint 158 | ); 159 | }); 160 | let updateStatus = []; 161 | newData.nodes.forEach((_newNode) => { 162 | let oldNode = _.find(oldData.nodes, (_oldNode) => { 163 | return _newNode.id === _oldNode.id; 164 | }); 165 | if (oldNode && oldNode.status !== _newNode.status) { 166 | updateStatus.push({ 167 | status: _newNode.status, 168 | node: oldNode 169 | }); 170 | } 171 | }); 172 | 173 | return { 174 | addGroups, 175 | rmGroups, 176 | addNodes, 177 | rmNodes, 178 | updateNodes, 179 | addEdges, 180 | rmEdges, 181 | updateStatus 182 | }; 183 | } 184 | -------------------------------------------------------------------------------- /src/canvas/canvas.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { Canvas, Layout } from 'butterfly-dag'; 3 | import AutoLayout, {calcGroupNodesPos} from '../utils/layout'; 4 | 5 | export default class MonitorCanvas extends Canvas { 6 | constructor(opts) { 7 | super(opts); 8 | 9 | // group的分页处理 10 | this._groupEnablePagination = opts.extraConfig.group.enablePagination 11 | this._groupPageSize = opts.extraConfig.group.pageSize; 12 | this._groupRowCnt = opts.extraConfig.group.rowCnt; 13 | this._onSearchGroup = opts.extraConfig.group.onSearchGroup; 14 | this._onChangePage = opts.extraConfig.onChangePage; 15 | } 16 | _addEventListener() { 17 | super._addEventListener(); 18 | this.on('custom.groups.changePage', (data) => { 19 | this.updateGroupPage(data.group); 20 | }) 21 | } 22 | addGroup(group, unionItems, options, isNotEventEmit, isNotRedrawByVirtualScroll) { 23 | let result = super.addGroup(group, unionItems, options, isNotEventEmit, isNotRedrawByVirtualScroll); 24 | result._onSearchGroup = this._onSearchGroup; 25 | return result; 26 | } 27 | redraw() { 28 | let nodes = this.nodes.map((item) => { 29 | return item.options; 30 | }); 31 | let groups = this.groups.map((item) => { 32 | return { 33 | id: item.id, 34 | width: item.width, 35 | height: item.height, 36 | isGroup: true 37 | } 38 | }); 39 | let edges = this.edges.map((item) => { 40 | return { 41 | sourceNode: item.sourceNode.id, 42 | targetNode: item.targetNode.id 43 | } 44 | }); 45 | 46 | AutoLayout({ 47 | nodes: nodes, 48 | groups: groups, 49 | edges: edges 50 | }, { 51 | rankdir: _.get(this.layout, 'options.rankdir') || 'TB', 52 | align: _.get(this.layout, 'options.align'), 53 | nodeSize: _.get(this.layout, 'options.nodeSize'), 54 | nodesepFunc: _.get(this.layout, 'options.nodesepFunc'), 55 | ranksepFunc: _.get(this.layout, 'options.ranksepFunc'), 56 | nodesep: _.get(this.layout, 'options.nodesep') || 50, 57 | ranksep: _.get(this.layout, 'options.ranksep') || 50, 58 | controlPoints: _.get(this.layout, 'options.controlPoints') || false, 59 | }, { 60 | rowCnt: this._groupRowCnt 61 | }); 62 | 63 | this.nodes.forEach((item, index) => { 64 | let newLeft = nodes[index].left; 65 | let newTop = nodes[index].top; 66 | if (item.top !== newTop || item.left !== newLeft) { 67 | item.options.top = newTop; 68 | item.options.left = newLeft; 69 | item.moveTo(newLeft, newTop); 70 | } 71 | }); 72 | 73 | this.groups.forEach((item, index) => { 74 | let newLeft = groups[index].left; 75 | let newTop = groups[index].top; 76 | if (item.top !== newTop || item.left !== newLeft) { 77 | item.options.top = newTop; 78 | item.options.left = newLeft; 79 | item.moveTo(newLeft, newTop); 80 | } 81 | }); 82 | 83 | 84 | this.focusCenterWithAnimate(); 85 | } 86 | 87 | updateGroupPage(group) { 88 | let showList = group._showNodeList; 89 | let currentNodesList = this.nodes.filter((item) => { 90 | return item.group === group.id; 91 | }); 92 | 93 | let rmNodes = currentNodesList.filter((item) => { 94 | return !_.some(showList, (_node) => item.id === _node.id); 95 | }); 96 | let addNodes = showList.filter((item) => { 97 | return !_.some(currentNodesList, (_node) => item.id === _node.id); 98 | }); 99 | 100 | const ROW_CNT = this._groupRowCnt || 5; 101 | calcGroupNodesPos(showList, { 102 | rowCnt: ROW_CNT 103 | }); 104 | 105 | this.removeNodes(rmNodes); 106 | this.addNodes(addNodes); 107 | 108 | this._onChangePage({ 109 | nodes: group._showNodeList, 110 | groupObj: group, 111 | pageNum: group._pageNum, 112 | pageSize: group._pageSize, 113 | totalNum: group._totalNum, 114 | keyword: group._keyword 115 | }) 116 | 117 | group._updateTotalCnt(); 118 | } 119 | }; 120 | -------------------------------------------------------------------------------- /src/canvas/edge.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {Edge} from 'butterfly-dag'; 4 | import $ from 'jquery'; 5 | import _ from 'lodash'; 6 | import * as ReactDOM from 'react-dom'; 7 | import { Tips } from 'butterfly-dag'; 8 | import RightMenuGen from './right-menu'; 9 | 10 | export default class BaseEdge extends Edge { 11 | mounted() { 12 | this._createRightMenu(); 13 | this._createTips(); 14 | if (_.get(this, 'options.flow')) { 15 | this.addAnimate({ 16 | color: '#4b96ef' 17 | }); 18 | } 19 | } 20 | draw(obj) { 21 | let path = super.draw(obj); 22 | path.setAttribute('class', 'butterflies-link monitor-dag-link'); 23 | return path; 24 | } 25 | drawArrow(arrow) { 26 | let path = super.drawArrow(arrow); 27 | if (path) { 28 | path.setAttribute('class', 'butterflies-arrow monitor-dag-arrow'); 29 | } 30 | return path; 31 | } 32 | focus() { 33 | $(this.dom).addClass('focus'); 34 | $(this.arrowDom).addClass('focus'); 35 | $(this.labelDom).addClass('focus'); 36 | } 37 | unfocus() { 38 | $(this.dom).removeClass('focus'); 39 | $(this.arrowDom).removeClass('focus'); 40 | $(this.labelDom).removeClass('focus'); 41 | } 42 | drawLabel(label) { 43 | if (label) { 44 | let labelRender = _.get(this, 'options._config.labelRender'); 45 | let container = $(''); 46 | container.on('click', (e) => { 47 | e.preventDefault(); 48 | e.stopPropagation(); 49 | this.emit('custom.edge.labelClick', { 50 | edge: this, 51 | label: label 52 | }); 53 | }); 54 | if (labelRender) { 55 | ReactDOM.render( 56 | labelRender(label, this.options), 57 | container[0] 58 | ); 59 | return container[0]; 60 | } else if (label) { 61 | container.text(label); 62 | return container[0]; 63 | } 64 | } 65 | } 66 | // 右键菜单 67 | _createRightMenu() { 68 | let menus = _.get(this, 'options._menu', []); 69 | if (menus.length > 0) { 70 | $(this.eventHandlerDom).contextmenu((e) => { 71 | e.preventDefault(); 72 | e.stopPropagation(); 73 | RightMenuGen(this.dom, 'edge', [e.clientX, e.clientY], menus, this.options); 74 | this.emit('custom.edge.rightClick', { 75 | edge: this 76 | }); 77 | }) 78 | } 79 | } 80 | // 生成提示 81 | _createTips() { 82 | let labelTipsRender = _.get(this, 'options._config.labelTipsRender'); 83 | if (labelTipsRender && this.labelDom) { 84 | let tipsDom = $('
'); 85 | ReactDOM.render( 86 | labelTipsRender(this.options.label, this.options), 87 | tipsDom[0] 88 | ); 89 | Tips.createTip({ 90 | placement: 'right', 91 | targetDom: this.labelDom, 92 | genTipDom: () => { 93 | return tipsDom[0]; 94 | } 95 | }); 96 | } 97 | } 98 | } 99 | 100 | -------------------------------------------------------------------------------- /src/canvas/endpoint.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Endpoint, Tips } from 'butterfly-dag'; 4 | import * as ReactDOM from 'react-dom'; 5 | import $ from 'jquery'; 6 | 7 | class NewEndPoint extends Endpoint { 8 | constructor(opts) { 9 | super(opts); 10 | } 11 | mounted() { 12 | // 生成提示 13 | this._createTips(); 14 | } 15 | _createTips() { 16 | let pointTipsRender = _.get(this, 'options._config.endpointTipsRender'); 17 | if (pointTipsRender) { 18 | let placement = ''; 19 | let direction = _.get(this, 'options._config.direction'); 20 | if (direction === 'left-right') { 21 | placement = this.type === 'target' ? 'left' : 'right'; 22 | } else { 23 | placement = this.type === 'target' ? 'top' : 'bottom'; 24 | } 25 | 26 | let tipsDom = $('
'); 27 | ReactDOM.render( 28 | pointTipsRender(this.options), 29 | tipsDom[0] 30 | ); 31 | Tips.createTip({ 32 | placement: placement, 33 | targetDom: this.dom, 34 | notEventThrough: true, 35 | genTipDom: () => { 36 | return tipsDom[0]; 37 | } 38 | }); 39 | } 40 | } 41 | } 42 | 43 | export default NewEndPoint; 44 | -------------------------------------------------------------------------------- /src/canvas/group.js: -------------------------------------------------------------------------------- 1 | import {Group} from 'butterfly-dag'; 2 | import $ from 'jquery'; 3 | import _ from 'lodash'; 4 | import RightMenuGen from './right-menu'; 5 | 6 | // const renderPagenation = (data) => { 7 | // const {currentPage, total, pageSize, isSearch, filterValue, pageCount} = data.options; 8 | // return
9 | // {isSearch ? : null} 10 | //
11 | // 12 | // {currentPage}/{pageCount} 13 | // 14 | //
15 | //
16 | // } 17 | 18 | class BaseGroup extends Group { 19 | constructor(opts) { 20 | super(opts); 21 | this._titleContainer = null; 22 | this._enableSearch = opts.enableSearch; 23 | this._enablePagination = opts._enablePagination; 24 | this._pageSize = opts._pageSize; 25 | this._pageNum = opts._pageNum; 26 | this._totalNum = opts._totalNum; 27 | this._showNodeList = opts._showNodeList; 28 | this._allNodeList = opts._allNodeList; 29 | this._searchNodesList = []; 30 | this._keyword = ''; 31 | } 32 | draw(obj) { 33 | let _dom = obj.dom; 34 | if (!_dom) { 35 | _dom = $('
') 36 | .attr('class', 'group') 37 | .css('top', obj.top) 38 | .css('left', obj.left) 39 | .css('width', obj.width + 'px') 40 | .css('height', obj.height + 'px') 41 | .attr('id', obj.id); 42 | } 43 | 44 | let group = $(_dom); 45 | this._container = $('
') 46 | .attr('class', 'butterflie-group'); 47 | let titleContainer = $('
'); 48 | 49 | group.append(titleContainer) 50 | // 添加文字 51 | if (_.get(obj, 'options.title')) { 52 | titleContainer.append(`${obj.options.title}`); 53 | } 54 | 55 | // 添加搜索 56 | if (this._enableSearch) { 57 | this._createSearch(titleContainer); 58 | } 59 | 60 | // 添加分页 61 | if(this._enablePagination) { 62 | this._createPagination(titleContainer); 63 | } 64 | 65 | group.append(this._container); 66 | 67 | this._titleContainer = titleContainer; 68 | 69 | return _dom; 70 | } 71 | 72 | mounted() { 73 | // 生成右键菜单 74 | this._createRightMenu(); 75 | } 76 | 77 | // 生成右键菜单 78 | _createRightMenu() { 79 | let menus = _.get(this, 'options._menu', []); 80 | if (menus.length > 0) { 81 | $(this.dom).contextmenu((e) => { 82 | e.preventDefault(); 83 | e.stopPropagation(); 84 | RightMenuGen(this.dom, 'groups', [e.clientX, e.clientY], menus, this.options); 85 | this.emit('custom.groups.rightClick', { 86 | groups: this 87 | }); 88 | }) 89 | } 90 | } 91 | 92 | // 生成搜索 93 | _createSearch (container = []) { 94 | let searchDom = [ 95 | `` 96 | ].join(''); 97 | searchDom = $(searchDom); 98 | $(container).append(searchDom); 99 | 100 | searchDom.on('click',(e) => { 101 | e.preventDefault(); 102 | e.stopPropagation(); 103 | $(e.target).focus(); 104 | }); 105 | 106 | searchDom.on('keydown',(e) => { 107 | if(e.keyCode === 13) { 108 | this._searchNodes($(e.target).val()); 109 | } 110 | }) 111 | } 112 | _searchNodes(text) { 113 | 114 | let result = []; 115 | if (!!text) { 116 | if (this._onSearchGroup) { 117 | result = this._onSearchGroup(text, this._allNodeList); 118 | } else { 119 | result = this._allNodeList.filter((item) => { 120 | return (item.title || '').indexOf(text) !== -1; 121 | }); 122 | } 123 | } else { 124 | result = this._allNodeList; 125 | } 126 | 127 | this._searchNodesList = result; 128 | this._keyword = text; 129 | this._pageNum = 1; 130 | this._totalNum = this._searchNodesList.length; 131 | this._showNodeList = this._searchNodesList.slice((this._pageNum - 1) * this._pageSize, this._pageNum * this._pageSize); 132 | this.emit('custom.groups.changePage', { 133 | group: this 134 | }); 135 | // let resultNode = this.nodes.filter((item) => { 136 | // console.log(item); 137 | // return _.get(item, 'options.title', '').indexOf(text) !== -1; 138 | // }); 139 | // this.emit('custom.group.search', { 140 | // nodes: focusNodes 141 | // }) 142 | } 143 | 144 | _createPagination(container = this._titleContainer) { 145 | let paginationDom = [ 146 | '
', 147 | '<', 148 | ``, 149 | '>', 150 | '
' 151 | ].join(''); 152 | 153 | paginationDom = $(paginationDom); 154 | $(container).append(paginationDom); 155 | 156 | paginationDom.find('.pre-page').on('click', () => { 157 | if (this._pageNum === 1) { 158 | return; 159 | } 160 | this._pageNum --; 161 | let _allNodeList = !!this._keyword ? this._searchNodesList : this._allNodeList; 162 | this._showNodeList = _allNodeList.slice((this._pageNum - 1) * this._pageSize, this._pageNum * this._pageSize); 163 | 164 | this.emit('custom.groups.changePage', { 165 | group: this 166 | }); 167 | 168 | }); 169 | 170 | paginationDom.find('.next-page').on('click', () => { 171 | let _allNodeList = !!this._keyword ? this._searchNodesList : this._allNodeList; 172 | let _lastPage = parseInt(_allNodeList.length / this._pageSize); 173 | if (_allNodeList.length % this._pageSize !== 0) { 174 | _lastPage ++; 175 | } 176 | 177 | if (this._pageNum === _lastPage) { 178 | return; 179 | } 180 | this._pageNum ++; 181 | this._showNodeList = _allNodeList.slice((this._pageNum - 1) * this._pageSize, this._pageNum * this._pageSize); 182 | 183 | this.emit('custom.groups.changePage', { 184 | group: this 185 | }); 186 | }); 187 | 188 | this._updateTotalCnt(container); 189 | } 190 | 191 | _updateTotalCnt (container = this._titleContainer) { 192 | let dom = $(container).find('.total-num'); 193 | let _allNodeList = !!this._keyword ? this._searchNodesList : this._allNodeList; 194 | let _cnt = parseInt(_allNodeList.length / this._pageSize); 195 | if (_allNodeList.length % this._pageSize !== 0) { 196 | _cnt ++; 197 | } 198 | dom.text(`${this._pageNum} / ${_cnt}`); 199 | } 200 | } 201 | export default BaseGroup; 202 | -------------------------------------------------------------------------------- /src/canvas/node.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Node } from 'butterfly-dag'; 4 | import $ from 'jquery'; 5 | import Endpoint from './endpoint'; 6 | import * as _ from 'lodash'; 7 | import * as ReactDOM from 'react-dom'; 8 | import RightMenuGen from './right-menu'; 9 | import { Tips } from 'butterfly-dag'; 10 | import getType from './../utils/getType'; 11 | export default class ScheduleNode extends Node { 12 | constructor(opts) { 13 | super(opts); 14 | // 当状态命中后会加上对应的类名 15 | this.statusCfg = { 16 | success: 'success', 17 | fail: 'fail', 18 | timeout: 'timeout', 19 | running: 'running', 20 | waiting: 'waiting', 21 | } 22 | this.status = undefined; 23 | this.statusDom = undefined; 24 | } 25 | draw(obj) { 26 | let _dom = obj.dom; 27 | if (!_dom) { 28 | _dom = $('
') 29 | .attr('class', 'node schedule-node') 30 | .attr('id', obj.name); 31 | let _className = _.get(obj, 'options.className'); 32 | if (_className) { 33 | $(_dom).addClass(_className); 34 | } 35 | } 36 | const node = $(_dom); 37 | // 计算节点坐标 38 | if (obj.top !== undefined) { 39 | node.css('top', `${obj.top}px`); 40 | } 41 | if (obj.left !== undefined) { 42 | node.css('left', `${obj.left}px`); 43 | } 44 | let direction = _.get(this, 'options._config.direction', 'top-bottom'); 45 | if (direction === 'left-right') { 46 | let leftPoint = $('
'); 47 | let rightPoint = $('
'); 48 | node.append(leftPoint).append(rightPoint); 49 | } else { 50 | let topPoint = $('
'); 51 | let bottomPoint = $('
'); 52 | node.append(topPoint).append(bottomPoint); 53 | } 54 | if (_.get(this, 'options._config.nodeRender')) { 55 | this._createCustomDom(node); 56 | } else { 57 | this._createStatusPoint(node); 58 | this._createTitle(node); 59 | } 60 | this.updateStatusPoint(node); 61 | return node[0]; 62 | } 63 | 64 | mounted() { 65 | // 生成锚点 66 | this._createNodeEndpoint(); 67 | // 生成右键菜单 68 | this._createRightMenu(); 69 | // 生成提示 70 | this._createTips(); 71 | // 绑定双击事件 72 | $(this.dom).on('dblclick', (e) => { 73 | this.emit('custom.node.dblClick', { 74 | node: this 75 | }); 76 | }); 77 | } 78 | 79 | focus() { 80 | $(this.dom).addClass('focus'); 81 | this.options.minimapActive = true; 82 | } 83 | 84 | unfocus() { 85 | $(this.dom).removeClass('focus'); 86 | this.options.minimapActive = false; 87 | } 88 | updateStatusPoint(container = this.dom) { 89 | let newStatus = _.get(this.statusCfg, this.options.status); 90 | let oldStatus = _.get(this.statusCfg, this.status); 91 | if (newStatus && newStatus !== oldStatus) { 92 | if (oldStatus) { 93 | $(container).removeClass(oldStatus) 94 | } 95 | $(container).addClass(newStatus); 96 | this.status = newStatus; 97 | } 98 | } 99 | _createStatusPoint(container = this.dom) { 100 | this.statusCfg = _.get(this, 'options._registerStatus', this.statusCfg); 101 | this.statusDom = $(''); 102 | $(container).append(this.statusDom); 103 | } 104 | _createTitle(container = this.dom) { 105 | let title = _.get(this, 'options.title'); 106 | if (title) { 107 | let titleDom = $(`${title}`); 108 | $(container).append(titleDom); 109 | } 110 | } 111 | _createCustomDom(container = this.dom) { 112 | let nodeRender = _.get(this, 'options._config.nodeRender'); 113 | ReactDOM.render( 114 | nodeRender(this.options), 115 | container[0], 116 | () => { 117 | (this.endpoints || []).forEach((item) => { 118 | item.updatePos(); 119 | }) 120 | } 121 | ); 122 | } 123 | // 生成锚点 124 | _createNodeEndpoint() { 125 | let direction = _.get(this, 'options._config.direction', 'top-bottom'); 126 | if (direction === 'left-right') { 127 | this.addEndpoint({ 128 | // id: this.id + '-input', 129 | originId: this.id, 130 | id: this.id, 131 | orientation: [-1,0], 132 | type: 'target', 133 | dom: $(this.dom).find('.left-point')[0], 134 | _config: _.get(this, 'options._config'), 135 | Class: Endpoint 136 | }); 137 | this.addEndpoint({ 138 | // id: this.id + '-output', 139 | id: this.id, 140 | originId: this.id, 141 | orientation: [1,0], 142 | type: 'source', 143 | dom: $(this.dom).find('.right-point')[0], 144 | _config: _.get(this, 'options._config'), 145 | Class: Endpoint 146 | }); 147 | } else { 148 | this.addEndpoint({ 149 | // id: this.id + '-input', 150 | id: this.id, 151 | originId: this.id, 152 | orientation: [0,-1], 153 | type: 'target', 154 | dom: $(this.dom).find('.top-point')[0], 155 | _config: _.get(this, 'options._config'), 156 | Class: Endpoint 157 | }); 158 | this.addEndpoint({ 159 | // id: this.id + '-output', 160 | id: this.id, 161 | originId: this.id, 162 | orientation: [0,1], 163 | type: 'source', 164 | dom: $(this.dom).find('.bottom-point')[0], 165 | _config: _.get(this, 'options._config'), 166 | Class: Endpoint 167 | }); 168 | } 169 | } 170 | // 生成右键菜单 171 | _createRightMenu() { 172 | let menus = _.get(this, 'options._menu'); 173 | let nodeMenuClassName = _.get(this, 'options._nodeMenuClassName', ''); 174 | if(['function', 'array'].includes(getType(menus))) { 175 | $(this.dom).contextmenu((e) => { 176 | e.preventDefault(); 177 | e.stopPropagation(); 178 | RightMenuGen(this.dom, 'node', [e.clientX, e.clientY], menus, this.options, nodeMenuClassName); 179 | this.emit('custom.node.rightClick', { 180 | node: this 181 | }); 182 | }) 183 | } 184 | } 185 | // 生成提示 186 | _createTips() { 187 | let nodeTipsRender = _.get(this, 'options._config.nodeTipsRender'); 188 | if (nodeTipsRender) { 189 | let tipsDom = $('
'); 190 | ReactDOM.render( 191 | nodeTipsRender(this.options), 192 | tipsDom[0] 193 | ); 194 | Tips.createTip({ 195 | placement: 'right', 196 | targetDom: this.dom, 197 | genTipDom: () => { 198 | return tipsDom[0]; 199 | } 200 | }); 201 | } 202 | } 203 | }; 204 | -------------------------------------------------------------------------------- /src/canvas/right-menu.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import $ from 'jquery'; 3 | import * as ReactDOM from 'react-dom'; 4 | import getType from '../utils/getType'; 5 | 6 | const Tips = require('butterfly-dag').Tips; 7 | 8 | let _genTipDom = (menu, data) => { 9 | let dom = $(''); 10 | let menuType = getType(menu); 11 | let menuData = menuType === 'function' ? menu(data) : menu; 12 | menuData.forEach((item) => { 13 | let menuItem = $(''); 14 | if (item.onClick) { 15 | menuItem.on('click', (e) => { 16 | item.onClick(item.key, data); 17 | }); 18 | } 19 | dom.append(menuItem); 20 | if (item.render) { 21 | ReactDOM.render( 22 | item.render(item.key, data), 23 | menuItem[0] 24 | ); 25 | } else { 26 | menuItem.text(item.title || item.key); 27 | } 28 | }); 29 | return dom[0] 30 | } 31 | 32 | export default (container, type, pos, menu, data, classname = '') => { 33 | Tips.createMenu({ 34 | className: `butterfly-${type}-monitor-menu ${classname}`, 35 | targetDom: container, 36 | genTipDom: () => { return _genTipDom(menu, data) }, 37 | placement: 'right', 38 | action: null, 39 | x: pos[0], 40 | y: pos[1], 41 | closable: true 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/react-monitor-dag/0071849ef9b7884c17e793a9da21fb0049c31f37/src/index.d.ts -------------------------------------------------------------------------------- /src/index.less: -------------------------------------------------------------------------------- 1 | @import './static/iconfont.css'; 2 | 3 | .butterfly-monitor-dag { 4 | background: #2E2E2E; 5 | .schedule-node { 6 | position: absolute; 7 | border: 1px solid #595959; 8 | color: #fff; 9 | height: 32px; 10 | min-width: 100px; 11 | line-height: 30px; 12 | border-radius: 16px; 13 | cursor: pointer; 14 | &:hover { 15 | background: rgba(255, 255, 255, 0.06); 16 | } 17 | .point { 18 | position: absolute; 19 | border-radius: 50%; 20 | width: 6px; 21 | height: 6px; 22 | border: 1px solid #595959; 23 | background: #2E2E2E; 24 | &.top-point { 25 | top: -4px; 26 | left: calc(~'50% - 4px'); 27 | } 28 | &.bottom-point{ 29 | bottom: -4px; 30 | left: calc(~'50% - 4px'); 31 | } 32 | &.left-point { 33 | top: calc(~'50% - 4px'); 34 | left: -4px; 35 | } 36 | &.right-point{ 37 | top: calc(~'50% - 4px'); 38 | right: -4px; 39 | } 40 | } 41 | .title { 42 | font-size: 12px; 43 | } 44 | .status { 45 | display: inline-block; 46 | width: 10px; 47 | height: 10px; 48 | border-radius: 50%; 49 | background: #595959; 50 | margin: 0px 5px; 51 | } 52 | &.success { 53 | border: 1px solid #60bfc5; 54 | .status { 55 | background: #60bfc5; 56 | } 57 | .point { 58 | border: 1px solid #60bfc5; 59 | } 60 | } 61 | &.fail { 62 | border: 1px solid #d6413f; 63 | .status { 64 | background: #d6413f; 65 | } 66 | .point { 67 | border: 1px solid #d6413f; 68 | } 69 | } 70 | &.running { 71 | border: 1px solid #4b96ef; 72 | .status { 73 | background: #4b96ef; 74 | } 75 | .point { 76 | border: 1px solid #4b96ef; 77 | } 78 | } 79 | &.waiting { 80 | border: 1px solid #6a40c6; 81 | .status { 82 | background: #6a40c6; 83 | } 84 | .point { 85 | border: 1px solid #6a40c6; 86 | } 87 | } 88 | &.timeout { 89 | border: 1px solid #ffd544; 90 | .status { 91 | background: #ffd544; 92 | } 93 | .point { 94 | border: 1px solid #ffd544; 95 | } 96 | } 97 | &.focus { 98 | border: 1px solid #F66902; 99 | box-shadow: 0px 0px 5px #f66902; 100 | background: rgba(255, 255, 255, 0.06); 101 | .point { 102 | border: 1px solid #F66902; 103 | } 104 | } 105 | } 106 | .monitor-dag-link{ 107 | &.focus { 108 | stroke: #F66902; 109 | } 110 | } 111 | .monitor-dag-label { 112 | transform: scale(0.8); 113 | background: #313131; 114 | border: 1px solid #828282; 115 | padding: 1px 6px; 116 | border-radius: 13px; 117 | color: #fff; 118 | cursor: pointer; 119 | &.focus { 120 | border: 1px solid #F66902; 121 | } 122 | } 123 | .monitor-dag-arrow { 124 | &.focus { 125 | stroke: #F66902; 126 | } 127 | } 128 | .butterflies-link-event-handler { 129 | &:hover { 130 | cursor: pointer; 131 | } 132 | } 133 | .monitor-canvas-action { 134 | background: #333; 135 | box-shadow: 0 0 9px 0 rgba(0, 0, 0, .5); 136 | position: absolute; 137 | right: 10px; 138 | top: 10px; 139 | z-index: 999; 140 | border: 1px solid #676565; 141 | div { 142 | height: 24px; 143 | width: 24px; 144 | text-align: center; 145 | line-height: 24px; 146 | cursor: pointer; 147 | color: #fff; 148 | opacity: .7; 149 | border-bottom: 1px solid #676565; 150 | i { 151 | -webkit-text-stroke-width: 0; 152 | font-size: 14px; 153 | } 154 | } 155 | div:hover{ 156 | background: #0f0f0f; 157 | } 158 | div:last-child { 159 | border-bottom: none; 160 | } 161 | } 162 | .status-container { 163 | position: absolute; 164 | top: 10px; 165 | left: 10px; 166 | .status-box { 167 | margin-right: 15px; 168 | color: #fff; 169 | .status-point { 170 | display: inline-block; 171 | width: 10px; 172 | height: 10px; 173 | border-radius: 50%; 174 | background: #595959; 175 | margin-right: 5px; 176 | &.success { 177 | background: #60bfc5; 178 | } 179 | &.fail { 180 | background: #d6413f; 181 | } 182 | &.running { 183 | background: #4b96ef; 184 | } 185 | &.waiting { 186 | background: #6a40c6; 187 | } 188 | &.timeout { 189 | background: #ffd544; 190 | } 191 | } 192 | } 193 | } 194 | 195 | .group{ 196 | border: 1px dashed white; 197 | .butterflie-group-title-content{ 198 | width: 100%; 199 | height: 42px; 200 | cursor: move; 201 | font-size: 14px; 202 | color: #FFF; 203 | line-height: 39px; 204 | padding-left: 12px; 205 | border-radius: 9px 9px 0 0; 206 | background: #545050; 207 | display: flex; 208 | justify-content: space-between; 209 | .group-pagination-wrap{ 210 | position: absolute; 211 | right: 10px; 212 | top: 8px; 213 | height: 24px; 214 | line-height: 24px; 215 | .group-pagination-wrap-prev, .group-pagination-wrap-next { 216 | min-width: 12px; 217 | margin: 0 4px; 218 | font-size: 18px; 219 | cursor: pointer; 220 | color: #b2aeb2; 221 | } 222 | .group-pagination-wrap-pager{ 223 | font-size: 18px; 224 | } 225 | } 226 | .group-search-input{ 227 | position: absolute; 228 | top: 11px; 229 | right: 100px; 230 | width: 100px; 231 | height: 24px; 232 | border-radius: 4px; 233 | border: 1px solid #353535; 234 | padding: 4px; 235 | background-color: #353535; 236 | font-size: 14px; 237 | } 238 | .group-search-input:focus { 239 | border: 1px solid #a7a4a4; 240 | border-radius: 4px; 241 | outline: none 242 | } 243 | } 244 | .butterflie-group { 245 | width: 100%; 246 | height: calc(100% - 42px); 247 | // background: rgba(246, 105, 2, 0.2); 248 | background: #5e5a5a; 249 | opacity: 0.5; 250 | border-radius: 0 0 9px 9px; 251 | } 252 | .schedule-node{ 253 | .title{ 254 | width: 100%; 255 | height: 30px; 256 | cursor: move; 257 | font-size: 12px; 258 | color: #FFF; 259 | line-height: 30px; 260 | padding-left: 4px; 261 | border-radius: 9px 9px 0 0; 262 | background: transparent; 263 | text-align: center; 264 | } 265 | } 266 | .pagination-con { 267 | position: absolute; 268 | top: 3px; 269 | right: 10px; 270 | .total-num { 271 | margin: 0 10px; 272 | } 273 | .pre-page, .next-page { 274 | cursor: pointer; 275 | } 276 | } 277 | } 278 | 279 | } 280 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as React from 'react'; 4 | import * as ReactDOM from 'react-dom'; 5 | import * as _ from 'lodash'; 6 | import './index.less'; 7 | import 'butterfly-dag/dist/index.css'; 8 | import Canvas from './canvas/canvas'; 9 | import Edge from './canvas/edge'; 10 | import {transformInitData, diffPropsData} from './adaptor'; 11 | import AutoLayout from './utils/layout'; 12 | 13 | // 右键菜单配置 14 | interface menu { 15 | title?: string, 16 | key: string, 17 | render?(key: string): void, 18 | onClick?(node: any): void, 19 | } 20 | 21 | // 画布配置 22 | interface config { 23 | delayDraw: number, // 延迟加载 24 | showActionIcon?: boolean,// 是否操作icon:放大,缩小,聚焦 25 | focusCenter?: boolean, //是否初始化的时候把所有节点居中 26 | draggable?: boolean, // 是否允许节点拖拽 27 | diffOptions?: Array, // 更新节点时,需要diff的字段集合(默认节点diff节点id) 28 | edge?: { //定制线段的类型,todo需要思考 29 | type: string, 30 | config: any, 31 | }, 32 | group?: { 33 | enableSearch: boolean, // 是否开启节点组搜索节点 34 | enablePagination: boolean, // 是否开启翻页 35 | pageSize: number, // 每页的数量 36 | rowCnt: number // 节点组每行展示多少个节点 37 | }, 38 | statusNote?: { 39 | enable: boolean, 40 | notes: [{ 41 | code: string, 42 | className: string, 43 | text: string, 44 | render?:() => JSX.Element 45 | }] 46 | }, 47 | labelRender?(label: string): JSX.Element, // 自定义label样式,没定义使用默认样式 48 | nodeRender?(data: any): JSX.Element, // 自定义节点样式,没定义使用默认样式 49 | onSearchGroup?(keyword: string, nodeList: any) 50 | autoLayout?: { 51 | enable: boolean, // 是否开启自动布局 52 | isAlways: boolean, // 是否添加节点后就重新布局, todo 53 | config: any // 算法配置 54 | }, 55 | minimap: { // 是否开启缩略图 56 | enable: boolean, 57 | config: { 58 | nodeColor: any 59 | } 60 | } 61 | } 62 | 63 | interface ComProps { 64 | data: any, // 画布数据 65 | width?: number | string, // 组件宽 66 | height?: number | string, // 组件高 67 | className?: string, // 组件classname 68 | nodeMenu: Array | ((node) => Array), // 节点右键菜单配置 69 | nodeMenuClassName?: string, // 节点菜单样式 70 | edgeMenu: Array, // 线段右键菜单配置 71 | groupMenu: Array, // group右键配置 72 | config?: config, // 画布配置 73 | polling?: { // 支持轮训 74 | enable: boolean, 75 | interval: number, 76 | getData(data): void 77 | }, 78 | registerStatus?: { // 自行注册状态,会根据node的status给节点加上class 79 | success: string, 80 | fail: string, 81 | // key:value的形式,可以自行注册,和node的status字段对应起来 82 | }, 83 | onClickNode?(node: any): void, // 单击节点事件 84 | onClickCanvas?():void, // 点击画布空白处事件 85 | onContextmenuNode?(node: any): void, // 右键节点事件 86 | onDblClickNode?(node: any): void, // 双击节点事件 87 | onClickEdge?(edge: any): void, // 单击线段事件 88 | onClickLabel?(label: string, edge: any): void, //单击label的事件 89 | onContextmenuEdge?(edge: any): void, // 右键线段事件 90 | onContextmenuGroup?(edge: any): void, // 右键线段事件 91 | onChangePage?(data:any): void, // 分页事件 92 | onLoaded?(data: any): void // 画布加载完成之后的回调 93 | onNodeStatusChange?(data: any): void // 画布有节点状态变化后的回调 94 | } 95 | 96 | export default class MonitorDag extends React.Component { 97 | protected canvas: any; 98 | protected canvasData: any; 99 | protected group: any; 100 | private _timer: any; 101 | private _focusNodes: any; 102 | private _focusLinks: any; 103 | private _statusNote: any; 104 | props: any; 105 | constructor(props: ComProps) { 106 | super(props); 107 | this.canvas = null; 108 | this.canvasData = null; 109 | 110 | this._focusNodes = []; 111 | this._focusLinks = []; 112 | 113 | this._timer = null; 114 | 115 | this._statusNote = { 116 | success: { 117 | className: 'success', 118 | text: '成功' 119 | }, 120 | fail: { 121 | className: 'fail', 122 | text: '失败' 123 | }, 124 | timeout: { 125 | className: 'timeout', 126 | text: '超时' 127 | }, 128 | running: { 129 | className: 'running', 130 | text: '正在运行' 131 | }, 132 | waiting: { 133 | className: 'waiting', 134 | text: '等待中' 135 | } 136 | }; 137 | } 138 | componentDidMount() { 139 | let root = ReactDOM.findDOMNode(this) as HTMLElement; 140 | if (this.props.width !== undefined) { 141 | root.style.width = this.props.width + 'px'; 142 | } 143 | if (this.props.height !== undefined) { 144 | root.style.height = this.props.height + 'px'; 145 | } 146 | let result = transformInitData({ 147 | config: this.props.config, 148 | nodeMenu: this.props.nodeMenu, 149 | edgeMenu: this.props.edgeMenu, 150 | groupMenu: this.props.groupMenu, 151 | nodeMenuClassName: this.props.nodeMenuClassName, 152 | data: _.cloneDeep(this.props.data || {nodes: [], edges: [], groups: []}), 153 | registerStatus: _.cloneDeep(this.props.registerStatus), 154 | groupCfg: _.get(this, 'props.config.group'), 155 | }); 156 | let canvasObj = ({ 157 | root: root, 158 | disLinkable: false, 159 | linkable: false, 160 | draggable: _.get(this.props, 'config.draggable', true), 161 | zoomable: true, 162 | moveable: true, 163 | theme: { 164 | edge: { 165 | // todo, 166 | type: 'endpoint', 167 | shapeType: _.get(this, 'props.config.edge.shapeType', 'AdvancedBezier'), 168 | isExpandWidth: true, 169 | arrow: _.get(this, 'props.config.edge.config.arrow', true), 170 | arrowPosition: _.get(this, 'props.config.edge.config.arrowPosition', 1), 171 | arrowOffset: _.get(this, 'props.config.edge.config.arrowPosition', -8), 172 | Class: Edge 173 | }, 174 | }, 175 | extraConfig: { 176 | onChangePage: _.get(this, 'props.onChangePage', () => {}), 177 | group: { 178 | enablePagination: _.get(this, 'props.config.group.enablePagination', true), 179 | pageSize: _.get(this, 'props.config.group.pageSize', 20), 180 | rowCnt: _.get(this.props, 'config.group.rowCnt', 5), 181 | onSearchGroup: _.get(this.props, 'config.onSearchGroup'), 182 | } 183 | } 184 | }); 185 | 186 | this.canvas = new Canvas(canvasObj); 187 | 188 | if (_.get(this.props, 'config.autoLayout.enable', false)) { 189 | let data = AutoLayout(result, { 190 | rankdir: _.get(this.props, 'config.direction', 'top-bottom') === 'top-bottom' ? 'TB' : 'LR', 191 | align: _.get(this.canvas.layout, 'options.align'), 192 | nodeSize: _.get(this.canvas.layout, 'options.nodeSize'), 193 | nodesepFunc: _.get(this.canvas.layout, 'options.nodesepFunc'), 194 | ranksepFunc: _.get(this.canvas.layout, 'options.ranksepFunc'), 195 | nodesep: _.get(this.canvas.layout, 'options.nodesep') || 50, 196 | ranksep: _.get(this.canvas.layout, 'options.ranksep') || 50, 197 | controlPoints: _.get(this.canvas.layout, 'options.controlPoints') || false, 198 | }, { 199 | rowCnt: _.get(this.props, 'config.group.rowCnt') 200 | }); 201 | 202 | } 203 | 204 | setTimeout(() => { 205 | this.canvas.draw(result, (data) => { 206 | this.props.onLoaded && this.props.onLoaded(data); 207 | let minimap:any = _.get(this, 'props.config.minimap', {}); 208 | const minimapCfg = _.assign({}, minimap.config, { 209 | events: [ 210 | 'system.node.click', 211 | 'system.canvas.click' 212 | ] 213 | }); 214 | if (minimap && minimap.enable) { 215 | this.canvas.setMinimap(true, minimapCfg); 216 | } 217 | if (_.get(this, 'props.config.focusCenter')) { 218 | this.canvas.focusCenterWithAnimate(); 219 | } 220 | }); 221 | }, _.get(this.props, 'config.delayDraw', 0)); 222 | 223 | this.canvas.on('events', (data) => { 224 | // console.log(data); 225 | }); 226 | this.canvasData = result; 227 | 228 | // 监听事件 229 | this.canvas.on('system.node.click', (data: any) => { 230 | this._focusNode([data.node]); 231 | this.props.onClickNode && this.props.onClickNode(data.node); 232 | }); 233 | 234 | this.canvas.on('custom.node.rightClick', (data: any) => { 235 | this.props.onContextmenuNode && this.props.onContextmenuNode(data.node); 236 | }); 237 | 238 | this.canvas.on('custom.node.dblClick', (data: any) => { 239 | this.props.onDblClickNode && this.props.onDblClickNode(data.node); 240 | }); 241 | 242 | this.canvas.on('custom.edge.rightClick', (data: any) => { 243 | this.props.onContextmenuEdge && this.props.onContextmenuEdge(data.edge); 244 | }); 245 | 246 | this.canvas.on('system.link.click', (data: any) => { 247 | this._focusLink([data.edge]); 248 | this.props.onClickEdge && this.props.onClickEdge(data.edge); 249 | }); 250 | 251 | this.canvas.on('custom.edge.labelClick', (data: any) => { 252 | this._focusLink([data.edge]); 253 | this.props.onClickLabel && this.props.onClickLabel(data.label, data.edge); 254 | }); 255 | 256 | this.canvas.on('system.canvas.click', (data: any) => { 257 | this._unfocus(); 258 | this.props.onClickCanvas && this.props.onClickCanvas(); 259 | }); 260 | 261 | this.canvas.on('custom.groups.rightClick', (data: any) => { 262 | this.props.onContextmenuGroup && this.props.onContextmenuGroup(data.groups); 263 | }); 264 | 265 | // 检测轮训 266 | this._polling(); 267 | } 268 | shouldComponentUpdate(newProps: ComProps, newState: any) { 269 | let result = transformInitData({ 270 | config: this.props.config, 271 | nodeMenu: this.props.nodeMenu, 272 | edgeMenu: this.props.edgeMenu, 273 | groupMenu: this.props.groupMenu, 274 | nodeMenuClassName: this.props.nodeMenuClassName, 275 | data: _.cloneDeep(newProps.data), 276 | registerStatus: _.cloneDeep(newProps.registerStatus), 277 | groupCfg: _.get(newProps, 'config.group'), 278 | }); 279 | let diffInfo = diffPropsData(result, this.canvasData, _.get(this, 'props.config.diffOptions', [])); 280 | if (diffInfo.rmGroups.length > 0) { 281 | this.canvas.removeGroups(diffInfo.rmGroups.map(item => item.id)); 282 | } 283 | if (diffInfo.addGroups.length > 0) { 284 | this.canvas.addGroups(diffInfo.addGroups); 285 | } 286 | if (diffInfo.rmNodes.length > 0) { 287 | this.canvas.removeNodes(diffInfo.rmNodes.map(item => item.id)); 288 | } 289 | if (diffInfo.addNodes.length > 0) { 290 | this.canvas.addNodes(diffInfo.addNodes); 291 | } 292 | if(diffInfo.updateNodes.length > 0) { 293 | let removeData = this.canvas.removeNodes(diffInfo.updateNodes.map(item => item.id), false, true); 294 | let _addNodes = this.canvas.addNodes(diffInfo.updateNodes, true); 295 | _addNodes.forEach(item => { 296 | item.mounted && item.mounted(); 297 | }); 298 | this.canvas.addEdges(removeData.edges.map(edge => { 299 | return edge.options; 300 | }), true); 301 | } 302 | if (diffInfo.addEdges.length > 0) { 303 | this.canvas.addEdges(diffInfo.addEdges); 304 | } 305 | if (diffInfo.rmEdges.length > 0) { 306 | this.canvas.removeEdges(diffInfo.rmEdges.map(item => item.id)); 307 | } 308 | if (diffInfo.updateStatus.length > 0) { 309 | let nodesUpdateStatus = diffInfo.updateStatus.map((item) => { 310 | let node = this.canvas.getNode(item.node.id); 311 | if (node) { 312 | node.updateStatusPoint(node.status); 313 | return { 314 | status: item.status, 315 | node 316 | } 317 | } 318 | }).filter((item) => item); 319 | this.props.onNodeStatusChange && this.props.onNodeStatusChange(nodesUpdateStatus); 320 | } 321 | 322 | this.canvasData = result; 323 | 324 | if ( 325 | _.get(this.props, 'config.autoLayout.isAlways', false) && ( 326 | diffInfo.addNodes.length > 0 || 327 | diffInfo.rmNodes.length > 0 || 328 | diffInfo.addEdges.length > 0 || 329 | diffInfo.rmEdges.length > 0 || 330 | diffInfo.addGroups.length > 0 || 331 | diffInfo.rmGroups.length > 0 332 | ) 333 | ) { 334 | this.canvas.redraw(); 335 | } 336 | 337 | // 检测轮训 338 | this._polling(newProps.polling); 339 | 340 | return true; 341 | } 342 | componentWillUnmount() { 343 | 344 | } 345 | 346 | render() { 347 | return ( 348 |
351 | {this._createStatusNote()} 352 | {this._createActionIcon()} 353 |
354 | ) 355 | } 356 | 357 | _createStatusNote() { 358 | let isShow = _.get(this, 'props.config.statusNote.enable', true); 359 | if (isShow) { 360 | let statusNote = _.get(this, 'props.config.statusNote.notes'); 361 | if (statusNote) { 362 | this._statusNote = statusNote; 363 | } 364 | let result = []; 365 | for(let key in this._statusNote) { 366 | if(typeof _.get(this._statusNote, `${key}.render`) === 'function') { 367 | result.push( 368 | 369 | {this._statusNote[key].render()} 370 | 371 | ) 372 | } else { 373 | result.push( 374 | 375 | 376 | {this._statusNote[key].text} 377 | 378 | ); 379 | } 380 | } 381 | return ( 382 |
383 | {result} 384 |
385 | ); 386 | } 387 | } 388 | _createActionIcon() { 389 | let isShow = _.get(this, 'props.config.showActionIcon', true); 390 | if (isShow) { 391 | return ( 392 |
393 |
{ 395 | this.canvas.zoom(this.canvas._zoomData + 0.1); 396 | }} 397 | > 398 | 399 |
400 |
{ 402 | this.canvas.zoom(this.canvas._zoomData - 0.1); 403 | }} 404 | > 405 | 406 |
407 |
{ 409 | this.canvas.focusCenterWithAnimate(); 410 | }}> 411 | 412 |
413 |
414 | ); 415 | } 416 | return null; 417 | } 418 | _genClassName() { 419 | let classname = ''; 420 | if (this.props.className) { 421 | classname = this.props.className + ' butterfly-monitor-dag'; 422 | } else { 423 | classname = 'butterfly-monitor-dag'; 424 | } 425 | return classname; 426 | } 427 | _polling(pollingCfg = this.props.polling || {}) { 428 | if (!pollingCfg.enable) { 429 | if (this._timer) { 430 | clearInterval(this._timer); 431 | } 432 | } else { 433 | if (!this._timer) { 434 | this._timer = setInterval(() => { 435 | pollingCfg.getData(this.canvas.getDataMap()); 436 | }, pollingCfg.interval); 437 | } 438 | } 439 | } 440 | // 聚焦节点 441 | _focusNode(nodes) { 442 | this._unfocus(); 443 | nodes.forEach((node) => { 444 | node.focus(); 445 | }); 446 | this._focusNodes = this._focusNodes.concat(nodes); 447 | } 448 | // 聚焦线段 449 | _focusLink(edges) { 450 | this._unfocus(); 451 | edges.forEach((edge) => { 452 | edge.focus(); 453 | }); 454 | this._focusLinks = this._focusLinks.concat(edges); 455 | } 456 | // 失焦 457 | _unfocus() { 458 | this._focusNodes.forEach((item) => { 459 | item.unfocus(); 460 | }); 461 | this._focusLinks.forEach((item) => { 462 | item.unfocus(); 463 | }); 464 | this._focusNodes = []; 465 | this._focusLinks = []; 466 | } 467 | } 468 | -------------------------------------------------------------------------------- /src/static/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face {font-family: "monitor-icon"; 2 | src: url('iconfont.eot?t=1617778754535'); /* IE9 */ 3 | src: url('iconfont.eot?t=1617778754535#iefix') format('embedded-opentype'), /* IE6-IE8 */ 4 | url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAV0AAsAAAAACxAAAAUnAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDcgqHcIcNATYCJAMkCxQABCAFhR0HfRvOCSMRtntQUpP9ZYHt/AEPaZMPdgsd6pBHjqL1J3tFx1DgyXXfXs+sbrUDv3keBJRXVSMYrHkg/CKeEAYHOwkQwLa4Ztse0IiouUM5++TRH6S9lNJP0ydwYzLRSq5yoAq4LXGBjw2d6043LfCBTywbq/BOd7r4AFkITKT+//1VHrBNm8jbhr3dP9B7f4hFg1kKVMFGDdQXJfP0rVta3pc5kZlAqkp9310KQwBuUsmLqFq9blNsNHKVAETfXj06YVeC6AGZwPaYHQ9qxEIMbDlLXgQWJN8v3lBY2CAxFFJUw+7VulLxuX71TE10kNqqiUxcLw5cHgMF5AU0iMmdybGorDEvCvfrbzQNAS82Ev3cejHhxZQXc149cxw81xWHNAu8RGpnMDHQK/7Dk1jYIEyIeFEwsMmG5zrQwoTnhpuEmuhmiFqohYIXE/wIplPQj0Q6B7Ww4NWzQdgAuKIHIoFYEHlBXEPYqmyqUEgEop2Jigrp9bpTYr2mmR6drLVJd0eGxY1ounQi22zZCigmcc1bxDfMhs8n3X75DapPoCeXkjLlfpevUpYvBcDobhr7Jh6XNOlWm51BUbYOfJAEy7R1zomF/oPZdGLLVmW79ynbqmW5HmOaHykSnNQc48/spvb4NbHZPYN7TosLFVo258DBbJiDvtkBEeOgmPhPRL7mlJi0DysACRVooz3UkwMKz9isdzdMhg4JlWHdCXdaePAIyZO4jCpqOSMmub1LGffS8TuxMSEr/r9d+6njka/svgR2P3H4kxvEy8zcvH1165Lf9mbaGWGfykOMAuHnFvXk086FFzCGdNCH9N+/6Dx0nnuo6Ot5UmCdunZNrWMkJnTwDlg9/dveSgsc1Mrf9mbY6w6sctq1v+2xb1m57uqVtQ3/lSthBWuvXF0ny/bN+v7ePfJF5hV60hS1d6+cLKecXVPOiHI1RU1uw1hfwMmnW4bna8rkPLMy6HNnazdNqb42pUZylV69qyZXT1nrMEbC8vDUGmtTqydX7dWrI+Ni68+d3ZZcLTnpjRBcGJhv3KD8LVvmHzTeYYx5m/ED87dqme+eV76Bc8536xwWlRIV1rlHWKGUQmE9eoG0V7ec2NZl29ltA+eXbF7ScUpt21bqybYnNPfsqyLKpCBKFiwophwxnT2/zNHPN+GbR8+R+/apOYoRE7oVZ1Yh35DDj8YUtYIk596Qd1wVtfHevY3YVt29qzfpjZzhRlVrWpEBCTlm7RE2EyYCONG5FZOP6eOqJoDcLgNwrrMDqgqAOlttxj/UMF//O6R1jynSIazsN9ujAbi7ZFRlqnVobASvSzFb/bjVOEe/7RJy6efUg6EIwEVDfHC9BNxaDQ5WR3J/XTbGTPDpbEwBEosMUHjJydGComDgoSKYeKkDbvLQ4nEPUUxGIbQLyM0yAEE8R0ASShAU8TzjaME/MIgVMWASL/LApbc+e8jwzrFvqghOiivMf0ALJ+PO0NvIiZSlqW33iraGSa7juO4TpRAug6Ht21peMKGseUv5tqOqASMc4Xnx4jAEhizscNF2U82nrjNdzhZfIu5uPRBootAKzP4AsmCJiRrVrZi56NrKRDP4814hqwomYphSV/wTvjnFTHsHBq3eyHkhyTzhr7Yvvlkj1isDguGCReD55+MoqIuZ5O64g9yoWtuM+uyk04WMlbTMxv9gDVLI+2yFaYbD5bFEd6PRZiFZAt4L2a004LJwLZXPh8hFaz5X/8yxoXTzVaeUKdnxutHAVXc7AA==') format('woff2'), 5 | url('iconfont.woff?t=1617778754535') format('woff'), 6 | url('iconfont.ttf?t=1617778754535') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */ 7 | url('iconfont.svg?t=1617778754535#monitor-icon') format('svg'); /* iOS 4.1- */ 8 | } 9 | 10 | .monitor-icon { 11 | font-family: "monitor-icon" !important; 12 | font-size: 16px; 13 | font-style: normal; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | .monitor-icon-left-circle:before { 19 | content: "\e77f"; 20 | } 21 | 22 | .monitor-icon-right-circle:before { 23 | content: "\e783"; 24 | } 25 | 26 | .monitor-icon-sousuo:before { 27 | content: "\e607"; 28 | } 29 | 30 | .monitor-icon-left:before { 31 | content: "\e606"; 32 | } 33 | 34 | .monitor-icon-right:before { 35 | content: "\e605"; 36 | } 37 | 38 | .monitor-icon-zoom-in:before { 39 | content: "\e604"; 40 | } 41 | 42 | .monitor-icon-quanping2:before { 43 | content: "\e78b"; 44 | } 45 | 46 | .monitor-icon-zoom-out:before { 47 | content: "\e9e5"; 48 | } 49 | 50 | -------------------------------------------------------------------------------- /src/static/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/react-monitor-dag/0071849ef9b7884c17e793a9da21fb0049c31f37/src/static/iconfont.eot -------------------------------------------------------------------------------- /src/static/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by iconfont 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/static/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/react-monitor-dag/0071849ef9b7884c17e793a9da21fb0049c31f37/src/static/iconfont.ttf -------------------------------------------------------------------------------- /src/static/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/react-monitor-dag/0071849ef9b7884c17e793a9da21fb0049c31f37/src/static/iconfont.woff -------------------------------------------------------------------------------- /src/static/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliyun/react-monitor-dag/0071849ef9b7884c17e793a9da21fb0049c31f37/src/static/iconfont.woff2 -------------------------------------------------------------------------------- /src/utils/getType.js: -------------------------------------------------------------------------------- 1 | const originToString = Object.prototype.toString; 2 | const getType = (data) => { 3 | let type = originToString.call(data); 4 | return type.match(/\[object (.*?)\]/)[1].toLowerCase(); 5 | }; 6 | 7 | export default getType; 8 | -------------------------------------------------------------------------------- /src/utils/layout.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'lodash'; 4 | 5 | import { Layout } from 'butterfly-dag'; 6 | 7 | const DEFUALT_NODE_WIDTH = 140; 8 | const DEFUALT_NODE_HEIGHT = 32; 9 | const DEFAULT_GROUP_NODE_W_INTERVAL = 15; 10 | const DEFAULT_GROUP_NODE_H_INTERVAL = 15; 11 | const DEFAULT_PADDING_L = 10; 12 | const DEFAULT_PADDING_T = 50; 13 | 14 | export const calcGroupNodesPos = (_groups, config = {}) => { 15 | const ROW_CNT = config.rowCnt || 5; 16 | let _groupsInfo = {}; 17 | for(let i = 0; i < _groups.length; i++) { 18 | let row = parseInt(i / ROW_CNT); 19 | let col = i % ROW_CNT; 20 | if (!_groupsInfo[row]) { 21 | _groupsInfo[row] = {w: 0, h: 0}; 22 | } 23 | let _nodeW = _groups[i].width || DEFUALT_NODE_WIDTH; 24 | let _nodeH = _groups[i].height || DEFUALT_NODE_HEIGHT; 25 | if (_nodeW > _groupsInfo[row].w) { 26 | _groupsInfo[row].w = _nodeW; 27 | } 28 | if (_nodeH > _groupsInfo[row].h) { 29 | _groupsInfo[row].h = _nodeH; 30 | } 31 | } 32 | 33 | _groupsInfo = Object.keys(_groupsInfo).sort().map((key) => _groupsInfo[key]); 34 | 35 | for(let i = 0; i < _groups.length; i++) { 36 | let row = parseInt(i / ROW_CNT); 37 | let col = i % ROW_CNT; 38 | 39 | let _top = 0; 40 | for(let j = 0; j <= row; j++) { 41 | _top += _groupsInfo[j].h; 42 | } 43 | _groups[i].top = _top + row * DEFAULT_GROUP_NODE_H_INTERVAL; 44 | _groups[i].left = col * _groupsInfo[row].w + col * DEFAULT_GROUP_NODE_W_INTERVAL; 45 | } 46 | 47 | 48 | // 加group的padding 49 | _groups.forEach((item) => { 50 | item.top += DEFAULT_PADDING_T; 51 | item.left += DEFAULT_PADDING_L; 52 | }); 53 | }; 54 | 55 | const layout = (data, options, extraOpts) => { 56 | let {nodes, groups, edges} = data; 57 | let layputOpts = {}; 58 | 59 | // 把节点组里面的节点过滤掉 60 | let innerNodes = nodes.filter((item) => item.group); 61 | let outerNodes = nodes.filter((item) => !item.group); 62 | let newEdges = edges.filter((item) => { 63 | return !_.some(innerNodes, (_node) => { 64 | return item.sourceNode === item.id || item.targetNode === item.id; 65 | }) 66 | }); 67 | // 把节点组当作一个大的节点 68 | outerNodes = outerNodes.concat(groups.map((item) => { 69 | item.isGroup = true; 70 | return item; 71 | })).map((item) => { 72 | if (!item.width) { 73 | item.width = DEFUALT_NODE_WIDTH; 74 | } 75 | if (!item.height) { 76 | item.height = DEFUALT_NODE_HEIGHT; 77 | } 78 | return item; 79 | }); 80 | 81 | let opts = _.assign({ 82 | data: { 83 | nodes: outerNodes, 84 | edges: newEdges.map((item) => { 85 | return { 86 | source: item.sourceNode, 87 | target: item.targetNode 88 | } 89 | }) 90 | } 91 | }, layputOpts, options); 92 | 93 | Layout.dagreLayout(opts); 94 | opts.data.nodes.forEach((item) => { 95 | item.top -= item.height / 2; 96 | item.left -= item.height / 2; 97 | }); 98 | 99 | // 再布局节点内部的节点 100 | let groupObj = {}; 101 | innerNodes.forEach((item) => { 102 | if (!groupObj[item.group]) { 103 | groupObj[item.group] = []; 104 | } 105 | groupObj[item.group].push(item); 106 | }); 107 | 108 | const ROW_CNT = extraOpts.rowCnt || 5; 109 | for(let key in groupObj) { 110 | let _groups = groupObj[key]; 111 | calcGroupNodesPos(_groups, { 112 | rowCnt: ROW_CNT 113 | }); 114 | } 115 | 116 | return { 117 | nodes, 118 | groups, 119 | edges 120 | } 121 | 122 | }; 123 | 124 | export default layout; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": false, 5 | "module": "commonjs", 6 | "target": "es5", 7 | "jsx": "react", 8 | "allowJs": true 9 | } 10 | } --------------------------------------------------------------------------------