使用 jsPlumb 绘制拓扑图 —— 异步加载与绘制的实现
本文实现的方法可以边异步加载数据边绘制拓扑图。主流程很简单: 发送 AJAX 请求获取数据 —> 创建节点(实际上就是DIV) —> 计算节点位置、布局 —> 添加节点附着点 —> 缓存节点连接 —> 连接所有现有的缓存节点连接。 多个 AJAX 请求的处理是异步的, 顺序没有控制。
本文实现的方法可以边异步加载数据边绘制拓扑图。 有若干点需要说明一下:
1. 一次性获取所有数据并绘制拓扑图, 请参见文章: <使用 JsPlumb 绘制拓扑图的通用方法> ; 本文实现的最终显示效果与之类似, 所使用的基本方法与之类似。
2. 在此次实现中, 可以一边异步加载数据一边绘制拓扑图, 是动态可扩展的;
3. 所有影响节点位置、布局的配置均放置在最前面, 便于修改, 避免在代码中穿梭, 浪费时间;
4. 布局算法比之前的实现更加完善;
5. 此实现由于与业务逻辑绑得比较紧, 可复用的部分不多, 但是可以作为一个模板, 用在读者自己的场景中, 自行修改相应的节点类型、URL等。
6. 添加了附着点的点击事件处理, 可以刷新显示关联实体;
7. 主流程很简单: 发送 AJAX 请求获取数据 —> 创建节点(实际上就是DIV) —> 计算节点位置、布局 —> 添加节点附着点 —> 缓存节点连接 —> 连接所有现有的缓存节点连接。 多个 AJAX 请求的处理是异步的, 顺序没有控制。
8. 代码:
1 /** 2 * 使用 jsPlumb 根据指定的拓扑数据结构绘制拓扑图 3 * 使用 drawTopo_asyn(vmName, regionNo, parentDivId) 方法 4 */ 5 /** 6 * 初始化拓扑图实例及外观设置 7 */ 8 (function() { 9 10 jsPlumb.importDefaults({ 11 12 DragOptions : { cursor: \'pointer\', zIndex:2000 }, 13 14 EndpointStyles : [{ fillStyle:\'#225588\' }, { fillStyle:\'#558822\' }], 15 16 Endpoints : [ [ "Dot", { radius:2 } ], [ "Dot", { radius: 2 } ]], 17 18 ConnectionOverlays : [ 19 [ "Label", { location:1 } ], 20 [ "Label", { 21 location:0.1, 22 id:"label", 23 cssClass:"aLabel" 24 }] 25 ] 26 }); 27 28 var connectorPaintStyle = { 29 lineWidth: 1, 30 strokeStyle: "#096EBB", 31 joinstyle:"round", 32 outlineColor: "#096EBB", 33 outlineWidth: 1 34 }; 35 36 var connectorHoverStyle = { 37 lineWidth: 2, 38 strokeStyle: "#5C96BC", 39 outlineWidth: 2, 40 outlineColor:"white" 41 }; 42 43 var endpointHoverStyle = { 44 fillStyle:"#5C96BC" 45 }; 46 47 window.topoDrawUtil = { 48 49 sourceEndpoint: { 50 endpoint:"Dot", 51 paintStyle:{ 52 strokeStyle:"#1e8151", 53 fillStyle:"transparent", 54 radius: 4, 55 lineWidth:2 56 }, 57 isSource:true, 58 maxConnections:-1, 59 connector:[ "Flowchart", { stub:[40, 60], gap:1, cornerRadius:5, alwaysRespectStubs:true } ], 60 connectorStyle: connectorPaintStyle, 61 hoverPaintStyle: endpointHoverStyle, 62 connectorHoverStyle: connectorHoverStyle, 63 dragOptions:{}, 64 overlays:[ 65 [ "Label", { 66 location:[0.5, 1.5], 67 label:"", 68 cssClass:"endpointSourceLabel" 69 } ] 70 ] 71 }, 72 73 targetEndpoint: { 74 endpoint: "Dot", 75 paintStyle: { fillStyle:"#1e8151",radius: 2 }, 76 hoverPaintStyle: endpointHoverStyle, 77 maxConnections:-1, 78 dropOptions:{ hoverClass:"hover", activeClass:"active" }, 79 isTarget:true, 80 overlays:[ 81 [ "Label", { location:[0.5, -0.5], label:"", cssClass:"endpointTargetLabel" } ] 82 ] 83 }, 84 85 initConnection: function(connection) { 86 connection.getOverlay("label").setLabel(connection.sourceId + "-" + connection.targetId); 87 connection.bind("editCompleted", function(o) { 88 if (typeof console != "undefined") 89 console.log("connection edited. path is now ", o.path); 90 }); 91 }, 92 93 removeAllEndPoints: function(nodeDivId) { 94 jsPlumb.removeAllEndpoints($(\'#\'+nodeDivId)); 95 }, 96 addEndpoints: function(toId, sourceAnchors, targetAnchors) { 97 for (var i = 0; i < sourceAnchors.length; i++) { 98 var sourceUUID = toId + sourceAnchors[i]; 99 var endPoint = jsPlumb.addEndpoint(toId, this.sourceEndpoint, { anchor:sourceAnchors[i], uuid:sourceUUID }); 100 endPoint.bind("click", function(endpoint) { 101 var anchorType = endpoint.anchor.type; 102 var nodeType = toId.split(\'_\')[0]; 103 var content = toId.split(\'_\')[1]; 104 if (nodeType == VM_TYPE) { 105 switch (anchorType) { 106 case \'Right\': 107 cacheKey = \'VM-DEVICE-\'+ vmNodeData.key; 108 cacheConnectionData[cacheKey] = null; 109 linkDevices(vmNodeData, vmNodeData.key); 110 break; 111 case \'Top\': 112 cacheKey = \'VM-ACCOUNT-\'+ vmNodeData.key; 113 cacheConnectionData[cacheKey] = null; 114 vmName = vmNodeData.key; 115 regionNo = vmNodeData.data.region_no; 116 linkAccount(vmNodeData, vmName, regionNo); 117 break; 118 case \'Bottom\': 119 cacheKey = \'VM-NC-\'+ vmNodeData.key; 120 cacheConnectionData[cacheKey] = null; 121 ncId = vmNodeData.data.nc_id; 122 regionNo = vmNodeData.data.region_no; 123 linkNc(vmNodeData, ncId, regionNo); 124 break; 125 case \'Left\': 126 cacheKey = \'VM-VIP-\'+ vmNodeData.key; 127 cacheConnectionData[cacheKey] = null; 128 vmInnerIp = vmNodeData.data.vm_inner_ip; 129 linkVips(vmNodeData, vmInnerIp); 130 break; 131 default: 132 break; 133 } 134 } 135 else if (nodeType == DEVICE_TYPE) { 136 if (anchorType == \'Bottom\') { 137 cacheKey = \'DEVICE-SNAPSHOT-\'+ content; 138 cacheConnectionData[cacheKey] = null; 139 deviceNodeData = deviceNodeDataMapping[content]; 140 linkSnapshot(deviceNodeData.data.aliUid, content, deviceNodeData); 141 } 142 } 143 }); 144 } 145 for (var j = 0; j < targetAnchors.length; j++) { 146 var targetUUID = toId + targetAnchors[j]; 147 jsPlumb.addEndpoint(toId, this.targetEndpoint, { anchor:targetAnchors[j], uuid:targetUUID }); 148 } 149 } 150 }; 151 })(); 152 ////////////////////////////////////////////////////////////////////////////// 153 // 这里将所有用到的数据结构汇聚在这里, 避免修改时需要在代码中穿行, 浪费时间 154 /** 155 * 重新刷新VM关联实体时需要使用到VM的信息,这里进行全局缓存,避免重复查询 156 * 重新刷新VM关联实体时VM必定存在, vmNodeData 也必定是最近一次查询的结果 157 */ 158 var vmNodeData = {}; 159 /** 160 * 重新刷新磁盘关联快照实体时需要使用到磁盘的信息,这里进行全局缓存,避免重复查询 161 * 重新刷新磁盘关联快照实体时磁盘必定存在,且必定是最近一次查询的结果 162 * eg. {\'instanceId\': { "ecsInstanceId": "vmName", "houyiDiskId": "102-80012003", 163 "aliUid": aliUidNum, "instanceId": "d-28ilj8rsf", ... }} 164 */ 165 var deviceNodeDataMapping = {}; 166 /** 167 * 拓扑图中的节点类型 168 */ 169 var nodeTypeArray = [\'VM\', \'DEVICE\', \'NC\', \'VIP\', \'SNAPSHOT\', \'CLUSTER\', \'AVZ\', \'ACCOUNT\']; 170 var VM_TYPE = nodeTypeArray[0]; 171 var DEVICE_TYPE = nodeTypeArray[1]; 172 var NC_TYPE = nodeTypeArray[2]; 173 var VIP_TYPE = nodeTypeArray[3]; 174 var SNAPSHOT_TYPE= nodeTypeArray[4]; 175 var CLUSTER_TYPE= nodeTypeArray[5]; 176 var AVZ_TYPE= nodeTypeArray[6]; 177 var ACCOUNT_TYPE= nodeTypeArray[7]; 178 /** 179 * cacheConnectionData 节点之间的已有连接数目缓存, 在计算节点位置及布局方法 computeLayout 中用到 180 * eg. { 181 * \'VM-DEVICE-vmkey\': 2, \'VM-NC-vmkey\':1, \'VM-VIP-vmkey\':2 , \'VM-ACCOUNT-vmkey\': 1, 182 * } 183 * 表示已经有2磁盘/1NC/2VIP/1ACCOUNT 与VM(key 为 vmkey)连接, 这些信息用于计算与VM相连接的同类型的下一个实体的相对位置 184 */ 185 var cacheConnectionData = {}; 186 /** 187 * 连接关系的存储, 在 cacheConnections , reconnectAll 方法中用到 188 * 由于重复节点会移动到新的位置,原有连接会出现断连现象, 因此采用"每次刷新拉取新实体时重连所有连线" 的策略, 可以保证实时性, 只要连接数不多重复连接的开销是可以接受的. 189 * connections = [[startPoint1, endPoint1], [startPoint2, endPoint2], ..., [startPointN, endPointN]]; 190 */ 191 var connections = []; 192 /** 193 * 实体与实体上附着点方向的设置 194 * DEVICE_TYPE: [[\'Right\', \'Bottom\'], [\'Left\']] 的含义是: 195 * 对于DEVICE实体: 作为起始节点时, 附着点可以在右方中点(连接CLUSTER), 下方中点(连接快照); 作为终止节点时, 附着点仅在左方中点(连接VM) 196 */ 197 var entityEndPointsMapping = { 198 "VM": [[\'Top\', \'Bottom\', \'Right\', \'Left\'], []], 199 "DEVICE": [[\'Right\', \'Bottom\'], [\'Left\']], 200 "NC": [[\'Bottom\'], [\'Top\']], 201 "VIP": [[], [\'Right\']], 202 "SNAPSHOT": [[], [\'Top\']], 203 "CLUSTER": [[], [\'Left\', \'Top\']], 204 "AVZ": [[\'Bottom\'], [\'Top\']], 205 "ACCOUNT": [[], [\'Bottom\']] 206 }; 207 /** 208 * 连接线附着点方向设置 209 * "VM-ACCOUNT": [\'Top\', \'Bottom\'] 的含义是: 210 * VM 的上方附着点 与 ACCOUNT 的下方附着点的连接 211 */ 212 var connectionDirectionMapping = { 213 "VM-ACCOUNT": [\'Top\', \'Bottom\'], 214 "VM-NC": [\'Bottom\', \'Top\'], 215 "NC-CLUSTER": [\'Bottom\', \'Top\'], 216 "VM-DEVICE": [\'Right\', \'Left\'], 217 "DEVICE-CLUSTER": [\'Right\', \'Left\'], 218 "VM-VIP": [\'Left\', \'Right\'], 219 "DEVICE-SNAPSHOT": [\'Bottom\', \'Top\'] 220 } 221 /** 222 * 节点之间的水平与垂直相对位置 223 */ 224 var largeVerticalDistance = 270; 225 var verticalDistance = 220; 226 var horizontalDistance = 300; 227 var shortVerticalDistance = 50; 228 var shortHorizontalDistance = 220; 229 /** 230 * 节点之间的水平或垂直相对位置和距离的设置 231 * "VM-DEVICE": [largeVerticalDistance, horizontalDistance] 232 */ 233 var connectionDistanceMapping = { 234 "VM-ACCOUNT": [-verticalDistance, 0], 235 "VM-NC": [shortVerticalDistance, 0], 236 "NC-CLUSTER": [shortVerticalDistance, 0], 237 "VM-DEVICE": [largeVerticalDistance, horizontalDistance], 238 "DEVICE-CLUSTER": [-108, horizontalDistance], 239 "VM-VIP": [verticalDistance, -horizontalDistance], 240 "DEVICE-SNAPSHOT": [shortVerticalDistance, shortHorizontalDistance] 241 } 242 /** 243 * 根节点位置 244 */ 245 rootPosition = [220, 360]; 246 rootTop = rootPosition[0]; 247 rootLeft = rootPosition[1]; 248 var parentDiv = null; 249 ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// 250 function drawtopo_asyn(vmName, regionNo, parentDivId) { 251 252 parentDiv = $(\'#\'+parentDivId); 253 254 var vmInfoReq = { 255 \'url\': httpPrefix + \'/controllers/vm/obtainVmData\', 256 \'params\' : { 257 \'vm_name\' : vmName, 258 \'region_no\': regionNo 259 } 260 }; 261 var vmjq = doAjaxRequest(vmInfoReq); 262 var vmInfoLoadedAfter = function(resultJson) { 263 264 vmNodeData = resultJson.result.data; 265 if (vmNodeData == null) { 266 alert(\'没有找到VM的相关信息!\'); 267 return ; 268 } 269 vmNode = createDiv(vmNodeData); 270 271 vmDivId = obtainNodeDivId(vmNodeData); 272 $(\'#\'+vmDivId).css(\'top\', rootTop + \'px\'); 273 $(\'#\'+vmDivId).css(\'left\', rootLeft + \'px\'); 274 275 linkAccount(vmNodeData, vmName, regionNo); 276 277 ncId = vmNodeData.data.nc_id; 278 linkNc(vmNodeData, ncId, regionNo); 279 280 // vmName = \'ATX-28n2dhdq8\'; 281 linkDevices(vmNodeData, vmName); 282 283 vmInnerIp = vmNodeData.data.vm_inner_ip; 284 linkVips(vmNodeData, vmInnerIp); 285 286 }; 287 vmjq.done(vmInfoLoadedAfter); 288 } 289 function linkAccount(vmNodeData, vmName, regionNo) { 290 var accountInfoReq = { 291 \'url\': httpPrefix + \'/controllers/vm/obtainAliyunAccountInfo\', 292 \'params\': { 293 \'vm_name\' : vmName, 294 \'region_no\': regionNo 295 } 296 }; 297 var accountjq = doAjaxRequest(accountInfoReq); 298 accountjq.done(function(resultJson) { 299 300 // for test 301 // resultJson = {"result":{"msg":"successful","code":200,"data":{"errorCode":null,"errorMsg":null,"aliyunID":"it-cloudpc@alibaba-inc.com","kp":null},"success":true}}; 302 303 if (resultJson.result.success) { 304 accountData = resultJson.result.data; 305 accountNodeData = createAccountData(accountData); 306 accountNode = createDiv(accountNodeData); 307 cacheConnections(vmNodeData, accountNodeData, obtainConnectionDirections(vmNodeData, accountNodeData)); 308 reconnectAll(connections); 309 } 310 else { 311 $(\'#error\').append(\'获取关联云账号信息失败!\'); 312 } 313 }).fail(function() { 314 $(\'#error\').append(\'获取关联云账号信息失败! \'); 315 }); 316 317 } 318 function linkNc(vmNodeData, ncId, regionNo) { 319 var ncInfoReq = { 320 \'url\': httpPrefix + \'/controllers/nc/listNc\', 321 \'params\': { 322 \'region_no\': regionNo, 323 \'nc_id\': ncId, 324 \'start\': 0, 325 \'page\': 1, 326 \'limit\': 1 327 } 328 }; 329 var ncjq = doAjaxRequest(ncInfoReq); 330 ncjq.done(function(resultJson) { 331 ncDataList = resultJson.data; 332 if (ncDataList.length > 0) { 333 ncData = ncDataList[0]; 334 ncNodeData = createNcData(ncData); 335 ncNode = createDiv(ncNodeData); 336 cacheConnections(vmNodeData, ncNodeData, obtainConnectionDirections(vmNodeData, ncNodeData)); 337 338 ncClusterNodeData = createNcClusterData(ncData); 339 ncClusterNode = createDiv(ncClusterNodeData); 340 cacheConnections(ncNodeData, ncClusterNodeData, obtainConnectionDirections(ncNodeData, ncClusterNodeData)); 341 reconnectAll(connections); 342 } 343 else { 344 $(\'#error\').append(\'获取关联NC实体失败!\'); 345 } 346 }).fail(function() { 347 $(\'#error\').append(\'获取关联NC实体失败!\'); 348 }); 349 } 350 function linkDevices(vmNodeData, vmName) { 351 var deviceInfoReq = { 352 \'url\' : httpPrefix + \'/controllers/disk/search\', 353 \'params\': { 354 \'vmName\': vmName 355 } 356 } 357 var regionPeNickName = vmNodeData.data.region_pe_nickname; 358 var devicejq = doAjaxRequest(deviceInfoReq); 359 devicejq.done(function(resultJson) { 360 361 total = resultJson.data.total; 362 if (total > 0) { 363 devices = resultJson.data.list; 364 365 for (var i=0; i<total; i++) { 366 367 deviceData = devices[i]; 368 deviceData[\'regionPeNickName\'] = regionPeNickName; 369 deviceNodeData = createDeviceData(deviceData); 370 deviceNodeDataMapping[deviceData.instanceId] = deviceNodeData; 371 deviceNode = createDiv(deviceNodeData); 372 cacheConnections(vmNodeData, deviceNodeData, obtainConnectionDirections(vmNodeData, deviceNodeData)); 373 374 deviceClusterNodeData = createDeviceClusterData(deviceData); 375 deviceClusterNode = createDiv(deviceClusterNodeData); 376 cacheConnections(deviceNodeData, deviceClusterNodeData, obtainConnectionDirections(deviceNodeData,deviceClusterNodeData)); 377 linkSnapshot(devices[i].aliUid, devices[i].instanceId, deviceNodeData); 378 } 379 reconnectAll(connections); 380 } 381 else { 382 $(\'#error\').append(\'该VM没有关联的磁盘实体!\'); 383 } 384 }).fail(function() { 385 $(\'#error\').append(\'获取关联磁盘实体失败!\'); 386 }); 387 } 388 function linkVips(vmNodeData, vmInnerIp) { 389 390 var vipInfoReq = { 391 \'url\': httpPrefix + \'/controllers/slbvip/listVip\', 392 \'params\': { 393 \'realserver_param\': vmInnerIp, 394 \'start\': 0, 395 \'page\': 1, 396 \'limit\': 100 397 } 398 }; 399 var vipjq = doAjaxRequest(vipInfoReq); 400 vipjq.done(function(resultJson) { 401 402 total = resultJson.total; 403 vips = resultJson.data; 404 if (total > 0) { 405 for (j=0; j<total; j++) { 406 var vipInfo = vips[j]; 407 vipNodeData = createVipData(vipInfo); 408 vipNode = createDiv(vipNodeData); 409 cacheConnections(vmNodeData, vipNodeData, obtainConnectionDirections(vmNodeData,vipNodeData)); 410 } 411 reconnectAll(connections); 412 } 413 else { 414 $(\'#error\').append(\'该VM没有关联的VIP实体!\'); 415 } 416 }).fail(function() { 417 $(\'#error\').append(\'获取关联VIP实体失败!\'); 418 }); 419 420 } 421 function linkSnapshot(aliUid, diskId, deviceNodeData) { 422 423 var snapshotInfoReq = { 424 \'url\': httpPrefix + \'/controllers/snapshot/search\', 425 \'params\': { 426 \'aliUid\': aliUid, 427 \'diskId\': diskId 428 } 429 }; 430 var snapshotjq = doAjaxRequest(snapshotInfoReq); 431 snapshotjq.done(function(resultJson) { 432 433 total = resultJson.total; 434 if (total > 0) { 435 snapshotDataList = resultJson.list; 436 for (k=0; k<total; k++) { 437 snapshotData = snapshotDataList[k]; 438 snapshotNodeData = createSnapshotData(snapshotData); 439 snapshotNode = createDiv(snapshotNodeData); 440 cacheConnections(deviceNodeData, snapshotNodeData, obtainConnectionDirections(deviceNodeData, snapshotNodeData)); 441 } 442 reconnectAll(connections); 443 } 444 else { 445 $(\'#error\').append(\'磁盘 \' + diskId + \' 没有关联的快照实体!\'); 446 } 447 }).fail(function() { 448 $(\'#error\').append(\'磁盘\' + diskId + \' 获取关联快照实体失败!\'); 449 }); 450 } 451 /** 452 * createXXXData 453 * 创建拓扑图所使用的节点数据 454 */ 455 function createVmData(vmData) { 456 return { 457 \'type\': \'VM\', 458 \'key\': vmData.vm_name, 459 \'data\': vmData 460 } 461 } 462 function createNcData(ncData) { 463 return { 464 \'type\': \'NC\', 465 \'key\': ncData.ip, 466 \'data\': ncData 467 } 468 } 469 function createNcClusterData(ncData) { 470 return { 471 \'type\': \'CLUSTER\', 472 \'key\': ncData.regionPeNickName, 473 \'data\': { 474 \'regionNo\': ncData.regionNo, 475 \'regionNickName\': ncData.regionNickName, 476 \'regionPeNickName\': ncData.regionPeNickName 477 } 478 } 479 } 480 function createDeviceData(deviceData) { 481 return { 482 \'type\': \'DEVICE\', 483 \'key\': deviceData.instanceId, 484 \'data\': deviceData 485 } 486 } 487 function createDeviceClusterData(deviceData) { 488 return { 489 \'type\': \'CLUSTER\', 490 \'key\': deviceData.regionNo, 491 \'data\': { 492 \'regionNo\': deviceData.regionNo 493 } 494 } 495 } 496 function createSnapshotData(snapshotData) { 497 return { 498 \'type\': \'SNAPSHOT\', 499 \'key\': snapshotData.snapshotId, 500 \'data\': snapshotData 501 } 502 } 503 function createSnapshotClusterData(snapshotData) { 504 return { 505 \'type\': \'CLUSTER\', 506 \'key\': snapshotData.regionNo, 507 \'data\': { 508 \'regionNo\': snapshotData.regionNo 509 } 510 } 511 } 512 function createVipData(vipData) { 513 return { 514 \'type\': \'VIP\', 515 \'key\': vipData.vipAddress, 516 \'data\': vipData 517 } 518 } 519 function createAccountData(accountData) { 520 return { 521 \'type\': \'ACCOUNT\', 522 \'key\': accountData.aliyunID, 523 \'data\': accountData 524 } 525 } 526 /** 527 * 缓存起始节点 beginNode 和终止节点 endNode 的连接关系 528 */ 529 function cacheConnections(beginNode, endNode, directions) { 530 531 computeLayout(beginNode, endNode); 532 533 var startPoint = obtainNodeDivId(beginNode) + directions[0]; 534 var endPoint = obtainNodeDivId(endNode) + directions[1]; 535 connections.push([startPoint, endPoint]); 536 } 537 /** 538 * 计算节点位置及布局 539 */ 540 function computeLayout(beginNode, endNode) { 541 542 var beginDivId = obtainNodeDivId(beginNode); 543 var endDivId = obtainNodeDivId(endNode); 544 var beginNodeType = beginNode.type; 545 var endNodeType = endNode.type; 546 547 beginNodeTop = $(\'#\'+beginDivId).offset().top; 548 beginNodeLeft = $(\'#\'+beginDivId).offset().left; 549 550 var key = beginNodeType + \'-\' + endNodeType + \'-\' + beginNode.key; 551 if (cacheConnectionData[key] == null) { 552 cacheConnectionData[key] = -1; 553 } 554 else { 555 cacheConnectionData[key] = cacheConnectionData[key]+1; 556 } 557 connNum = cacheConnectionData[key]; 558 559 var typeKey = beginNodeType + \'-\' + endNodeType; 560 var relDistance = connectionDistanceMapping[typeKey]; 561 var relVertiDistance = relDistance[0]; 562 var relHoriDistance = relDistance[1]; 563 564 switch (beginNodeType) { 565 case VM_TYPE: 566 if (endNodeType == VIP_TYPE) { 567 endNodePosition = [beginNodeTop+connNum*relVertiDistance, beginNodeLeft+relHoriDistance]; 568 } 569 else if (endNodeType == DEVICE_TYPE) { 570 endNodePosition = [beginNodeTop+connNum*relVertiDistance, beginNodeLeft+relHoriDistance]; 571 } 572 else if (endNodeType == NC_TYPE) { 573 endNodePosition = [beginNodeTop+relVertiDistance, beginNodeLeft+relHoriDistance]; 574 } 575 else if (endNodeType == ACCOUNT_TYPE) { 576 endNodePosition = [beginNodeTop+relVertiDistance, beginNodeLeft+relHoriDistance]; 577 } 578 break; 579 case DEVICE_TYPE: 580 if (endNodeType == CLUSTER_TYPE) { 581 endNodePosition = [beginNodeTop+relVertiDistance, beginNodeLeft+relHoriDistance]; 582 } 583 else if (endNodeType == SNAPSHOT_TYPE) { 584 endNodePosition = [beginNodeTop+relVertiDistance, beginNodeLeft+(connNum+1)*relHoriDistance]; 585 } 586 break; 587 case VIP_TYPE: 588 break; 589 case NC_TYPE: 590 if (endNodeType == CLUSTER_TYPE) { 591 endNodePosition = [beginNodeTop+relVertiDistance, beginNodeLeft+relHoriDistance]; 592 } 593 break; 594 case SNAPSHOT_TYPE: 595 default: 596 break; 597 } 598 599 $(\'#\'+endDivId).css(\'top\', endNodePosition[0] + \'px\'); 600 $(\'#\'+endDivId).css(\'left\', endNodePosition[1] + \'px\'); 601 602 addEndPoints(beginDivId, beginNodeType); 603 addEndPoints(endDivId, endNodeType); 604 } 605 /** 606 * 为节点添加用于连线的附着点 607 * @param nodeDivId 节点的 DIV ID 608 * @param type 节点类型 609 */ 610 function addEndPoints(nodeDivId, type) { 611 var startAttachedPoints = entityEndPointsMapping[type][0]; 612 var endAttachedPoints = entityEndPointsMapping[type][1]; 613 topoDrawUtil.addEndpoints(nodeDivId, startAttachedPoints, endAttachedPoints); 614 } 615 /** 616 * 连接所有连线 617 */ 618 function reconnectAll(connections) { 619 620 var i=0; 621 for (i=0; i<connections.length; i++) { 622 jsPlumb.connect({uuids:connections[i], editable: false}); 623 } 624 // 使所有拓扑节点均为可拉拽的 625 jsPlumb.draggable(jsPlumb.getSelector(".node"), { grid: [5, 5] }); 626 } 627 /** 628 * div id cache , avoid duplicated div. 629 * {\'divId\': \'divStr\'} 630 */ 631 divIdCache = {}; 632 /** 633 * 为节点数据创建节点\附着点并返回节点的DIV 634 */ 635 function createDiv(metaNode) { 636 var clickUrl = \'\'; 637 var display = \'\'; 638 var type = metaNode.type; 639 var regionPeNickname = \'\'; 640 if (metaNode.data != null) { 641 regionPeNickname = metaNode.data.regionPeNickName; 642 } 643 644 nodeDivId = obtainNodeDivId(metaNode); 645 646 if (divIdCache[nodeDivId] != null) { 647 // 该节点要移动到新的位置, 因此原来的附着点要去掉 648 topoDrawUtil.removeAllEndPoints(nodeDivId); 649 return divIdCache[nodeDivId]; 650 } 651 652 switch(type.toUpperCase()) { 653 case VM_TYPE: 654 clickUrl = httpPrefix + \'/framehtml/vm_monitor.html?vm_name=\' + metaNode.key + \'&data=\'+JSON.stringify(metaNode.data).replace(/\"/g,"\'"); 655 display = metaNode.key; 656 break; 657 case DEVICE_TYPE: 658 displayDevice1 = metaNode.data.instanceId; 659 clickDeviceUrl2 = httpPrefix + \'/framehtml/device_monitor.html?device_id=\' + metaNode.data.houyiDiskId + \'®ion_pe_nickname=\'+regionPeNickname; 660 displayDevice2 = metaNode.data.houyiDiskId; 661 break; 662 case NC_TYPE: 663 var regionNo = metaNode.data.regionNo; 664 clickUrl = httpPrefix + \'/framehtml/nc_monitor.html?nc_ip=\' + metaNode.key + \'®ion_pe_nickname=\'+regionPeNickname + \'®ion_no=\'+regionNo; 665 display = metaNode.key; 666 break; 667 case VIP_TYPE: 668 display = metaNode.key + \':\' + metaNode.data.port; 669 clickUrl = httpPrefix + \'/framehtml/vip_monitor.html?vip=\' + display + \'®ion_pe_nickname=\'+regionPeNickname + \'&slbdb_configId=\'+metaNode.data.slbdb_configId; 670 break; 671 case SNAPSHOT_TYPE: 672 display = metaNode.key + \'<br/>\' + metaNode.data.houyiSnapshotId + \'<br/><span style="color:green">\'+ metaNode.data.regionNo + \'</span>\'; 673 break; 674 case CLUSTER_TYPE: 675 case AVZ_TYPE: 676 case ACCOUNT_TYPE: 677 display = metaNode.key; 678 break; 679 default: 680 break; 681 } 682 683 if (type == VM_TYPE || type == NC_TYPE || type == VIP_TYPE || type == ACCOUNT_TYPE ) { 684 divStr = \'<div class="node biggerNode" id="\' + nodeDivId + \'"><strong>\' 685 + metaNode.type + \'<br/><a href="\' + clickUrl + \'" target="_blank">\' + display + \'</a><br/></strong></div>\'; 686 } 687 else if (type == DEVICE_TYPE){ 688 divStr = \'<div class="node biggerNode" id="\' + nodeDivId + \'"><strong>\' 689 + metaNode.type + \'<br/>\' + displayDevice1 + \'<br/><a href="\' + clickDeviceUrl2 + \'" target="_blank">\' + displayDevice2 + \'</a><br/></strong></div>\'; 690 } 691 else { 692 divStr = \'<div class="node biggerNode" id="\' + nodeDivId + \'"><strong>\' 693 + metaNode.type + \'<br/>\' + display + \'<br/></strong></div>\'; 694 } 695 parentDiv.append(divStr); 696 697 divIdCache[nodeDivId] = divStr; 698 return divStr; 699 } 700 function obtainConnectionDirections(srcNodeData, destNodeData) { 701 var key = srcNodeData.type + \'-\' + destNodeData.type; 702 var startDirection = connectionDirectionMapping[key][0]; 703 var endDirection = connectionDirectionMapping[key][1]; 704 return [startDirection, endDirection]; 705 } 706 /** 707 * 生成节点的 DIV id 708 * divId = nodeType.toUpperCase + "_" + key 709 * key 可能为 IP , 其中的 . 将被替换成 ZZZ , 因为 jquery id 选择器中 . 属于转义字符. 710 * eg. {type: \'VM\', key: \'1.1.1.1\' }, divId = \'VM_1ZZZ1ZZZ1ZZZ1\' 711 */ 712 function obtainNodeDivId(metaNode) { 713 if (metaNode.type == VIP_TYPE) { 714 return metaNode.type.toUpperCase() + \'_\' + transferKey(metaNode.key) + \'_\' + metaNode.data.port; 715 } 716 return metaNode.type.toUpperCase() + \'_\' + transferKey(metaNode.key); 717 } 718 function transferKey(key) { 719 return key.replace(/\./g, \'ZZZ\').replace(/\@/g,\'YYY\'); 720 } 721 function revTransferKey(value) { 722 return value.replace(/ZZZ/g, \'.\').replace(\'/YYY/g\',\'@\'); 723 }