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