背景
拓扑图是Console界面中一个重要的组成部分,它的愿景是我公司Console平台相比于其他公司相同平台的创新之处。这个部分一直是由我来负责,从开始的高度抽象派图形化图,到后面创新性的引入Radial-layout,到这轮迭代返璞归真的Tree-layout信息展示,几经迭代,感慨良多。很多时候都要跟设计师死磕,在他的想法与技术之间找到一个权衡点。本文作为本轮迭代的一个小小总结,也作为API文档的供其他同事参考。也因为要作为API文档,因此本文会深入到代码级讲述每段代码甚至函数和关键变量的作用。
早期版本的拓扑图
目标
拓扑图的目标是利用图形化的方式清晰地展示集群内App、Service、Deployment、Pod、Storage之间的关系,并提供交互手段给用户动态的增减Pod,查看某部分的Detail信息等。
概览
最新版本的拓扑图效果如下图所示:
最新版本的拓扑图
请注意,图片中除了后期加上的说明文字和边框外,其他的一切元素都是使用d3.js这个库绘制上去的,包括其中的文字。
从总体上看,拓扑图由三个部分组成,分别是顶部的样例说明区、主体部分的信息展示区、右上角的细节信息展示区。画布名称是vis,这是一个很关键的全局变量。
样例说明区
和另外两个部分不同,样例说明区是静态的,不会因为不同的App而变化。因此这个部分相对简单,涉及到的代码也不是很多。主要的实现方式是首先绘制一个无边框的圆角矩形放置在底部,然后用代码控制圆圈和相应文字的间隔和类型。类型是为了方便将css样式应用到图形上做的准备。核心代码如下:
123456789101112131415161718192021222324252627282930313233343536373839404142434445
vis.append('rect') .attr('x',-18) .attr('y',-30) .attr('rx',5) .attr('ry',5) .attr('width',4*180+120) .attr('height',30) .attr('stroke','none') .style('fill','#f0f0f0'); var exampleData = ['App','Service','Deployment','Pod','Storage']; var example = vis.selectAll('.example').data(exampleData).enter().append('g'); example.append('circle') .attr('cy',-15) .attr('cx',function(d,i){ return 180*(i); }) .style('fill','#fff') .attr('r',7) .attr('class',function(d,i){ if(i == 0){ return 'app' }else if(i ==1){ return 'service' }else if(i ==2){ return 'deployment' }else if(i ==3){ return 'pod' }else if(i ==4){ return 'storage' }else{ return 'unclear' } });example.append("text").attr('class','exampleTEXT').attr("x", function(d,i) { return 180*i+10 }).attr("y", function(d) { return -15; }).attr("dy", ".35em").attr("text-anchor", function() { return "start"; }).text(function(d) { return d; }).style("fill-opacity", 1);
主体信息展示区
主体信息的展示是拓扑图的核心部分。各个组件的数量会有很大的变化范围,这也是我们弃用老版本拓扑图的原因。坦率的讲,老版本的拓扑图是比较美观的、图形化的,但是遇到的核心问题是当某些组件数量过大时,容易引起大量的重叠问题,造成整个系统不work了。
而这个版本的拓扑图得益于d3提供的Tree布局方式,比较好的解决了这个问题。它会根据用户指定的画布宽高,算出最适合的布局方式,即每个点应该放置在哪里。
Tree-layout接受的数据形式如下图所示:
Tree-layout接受的数据格式
我们backend提供的json文件与要求的数据格式有较大的差别,下面比较恶心的代码的功能是利用循环将后端提供的json文件转化成Tree-layout需要的格式。在转换过程中,为了实现深拷贝功能,实现了deepClone函数,为方便起见,用了JSON的stringfy和parse方法编码解码。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
d3.json("topologyRaw.json", function(error, data) { if (error) throw error; var jsonData = {name:"nil",children:new Array()}; apps = data.app; storages = data.storages; services = data.services; deployments = data.deployments; var tmpDeployment; jsonData.name = data.app.name; for(var i =0 ;i<services.length;i++){ var service = new Array(); for(var j=0;j<deployments.length;j++){ var deployment = new Array(); if(contains(deployments[j]["selector"]["matchLabels"],services[i]["selector"])){ var pod = new Array(); for(var k=0;k<deployments[j]["pods"].length;k++){ var storage = new Array(); for(var m=0;m<deployments[j]["pods"][k]["storageNames"].length;m++) { for (var n = 0; n < storages.length; n++) { if(deployments[j]["pods"][k]["storageNames"][m]==storages[n]["storageName"]) { if(storages[n].unshared==true && storages[n]["storageType"]=="HostPath") { for(var q=0;q<storages[n]["hostpathInfo"].length;q++) { storage.push({ name: storages[n]["storageName"], size: storages[n]["hostpathInfo"][q]["amountBytes"] }) } } else{ storage.push({ name: deployments[j]["pods"][k]["storageNames"][m], size: getAmountByStorageName(deployments[j]["pods"][k]["storageNames"][m]) }) } } } } pod.push({name:deployments[j]["pods"][k].name,children:deepClone(storage)}); } tmpDeployment = deployments[j].name; } deployment.push({name:(tmpDeployment),children:deepClone(pod)}); } jsonData.children.push({name:services[i].name,children:deepClone(deployment)}); } function deepClone(initalObj) { var obj = {}; obj = JSON.parse(JSON.stringify(initalObj)); return obj; }
节点的样式和展开功能
为了让各个组件的样式满足设计师的严苛要求,需要根据Tree-Layout的depth(深度)进行判断,控制圆形的颜色、说明文字出现的位置(放置位置和对齐方式)。这部分逻辑是在nodeEnter、nodeUpdate和nodeExit相关变量后面指定的。也是凭借这三个变量之间的反差,每次页面刚打开并load的时候,会有一个舒展开的样式,非常美观。
展开的过程
节点的开关功能
同样,展开后,后期依然可以控制某个节点的子节点的开合状态,点击某个节点,它的子节点所包含的元素会以动画的形式收缩进它的位置,该节点的中心部分也会由白色变为浅灰色,并且整个布局会相应发生变化。再次点击,会执行相反的操作,收缩进去的部分会再次舒展开。这个逻辑是由toggle和update函数完成的。toggle函数的会将d.children保存到另一个变量_children中,然后将d.children置空(或执行相反操作),然后调用update函数重新绘制Tree的分支。
123456789101112131415161718192021222324
toggle = function(d) { if(d) { if (d.children) { d._children = d.children; return d.children = null; } else { d.children = d._children; return d._children = null; } }};update = function(source) { var duration, link, node, nodeEnter, nodeExit, nodeUpdate, nodes; duration = d3.event && d3.event.altKey ? 5000 : 500; nodes = tree.nodes(root).reverse(); nodes.forEach(function(d) { return d.y = d.depth * 180; }); node = vis.selectAll("g.node").data(nodes, function(d) { return d.id || (d.id = ++i); }).attr('id',function(d){ return d.name; });
增删pod的功能
增删pod的功能是在每条deployment与pod之间的加入两张预先做好的图片,监听click事件,调用相应的后端API,实现增删的功能。值得注意的是,圆环形、图片、文字说明是depolyment的一个g,点击图片默认情况下会造成deployment关闭状态。因此要在click事件中加入d3.event.stopPropagation();让点击事件不再向下传播。
hover高亮效果
注意到,当鼠标hover到某个元素上时,元素代表的text会高亮,从APP开始经该元素到达storage的路径都将会被高亮。这个部分主要是由highlightOn和highlightOff这两个函数控制的。
在建立路径的时候,我们给每一段路径都加上一个一个id,由path的起始点到path的终点id+name组成,这样就可以唯一确定该path。
id是: path.link.source-“ + d.parent.name+d.parent.id + “.target-“ + d.name+d.id
hightlightOn函数中,有两个重要的子函数,parentLine()和childLine(),这两个函数使用了递归的方式寻找从App到该节点和从该节点到storage节点的路径,并给特定路径指定highlight class,或取消该class的影响。
|
|
detail信息
Detail的信息,之前的博客中写过了,可以参考前面的博文。
原文作者: Chih-Hao
原文链接: http://zhihaozhang.github.io/2017/10/18/topology/
发表日期: October 18th 2017, 3:31:13 pm
版权声明: 本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可
-
Next Post谷歌开发者大会(GDD2017)见闻 Day1
-
Previous PostSwift4字典和集合的新特性