github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/web/public/index.mjs (about)

     1  import "./d3.v355.min.js";
     2  import { Graph, GraphNode } from './graph.mjs';
     3  
     4  const iconsModulePromise = import("./mdi-svg.min.js");
     5  
     6  export function renderPipeline(jobs, resources, newUrl){
     7    const foundSvg = d3.select(".pipeline-graph");
     8    const svg = createPipelineSvg(foundSvg)
     9    if (svg.node() != null) {
    10      draw(svg, jobs, resources, newUrl);
    11    }
    12  }
    13  
    14  var currentHighlight;
    15  
    16  var redraw;
    17  function draw(svg, jobs, resources, newUrl) {
    18    redraw = redrawFunction(svg, jobs, resources, newUrl);
    19    redraw();
    20  }
    21  
    22  function redrawFunction(svg, jobs, resources, newUrl) {
    23    return function() {
    24      // reset viewbox so calculations are done from a blank slate.
    25      //
    26      // without this text and boxes jump around on every redraw,
    27      // in affected browsers (seemingly anything but Chrome + OS X).
    28      d3.select(svg.node().parentNode).attr("viewBox", "0 0 0 0");
    29  
    30      var graph = createGraph(svg, jobs, resources);
    31  
    32      svg.selectAll("g.edge").remove();
    33      svg.selectAll("g.node").remove();
    34  
    35      var svgEdges = svg.selectAll("g.edge")
    36        .data(graph.edges());
    37  
    38      svgEdges.exit().remove();
    39  
    40      var svgNodes = svg.selectAll("g.node")
    41        .data(graph.nodes());
    42  
    43      svgNodes.exit().remove();
    44  
    45      var svgEdge = svgEdges.enter().append("g")
    46        .attr("class", function(edge) { return "edge " + edge.source.node.status })
    47  
    48      svgEdges.each(function(edge) {
    49        if (edge.customData !== null && edge.customData.trigger !== true) {
    50          d3.select(this).classed("trigger-false", true)
    51        }
    52      })
    53  
    54      function highlight(thing) {
    55        if (!thing.key) {
    56          return
    57        }
    58  
    59        currentHighlight = thing.key;
    60  
    61        svgEdges.each(function(edge) {
    62          if (edge.source.key == thing.key) {
    63            d3.select(this).classed({
    64              active: true
    65            })
    66          }
    67        })
    68  
    69        svgNodes.each(function(node) {
    70          if (node.key == thing.key) {
    71            d3.select(this).classed({
    72              active: true
    73            })
    74          }
    75        })
    76      }
    77  
    78      function lowlight(thing) {
    79        if (!thing.key) {
    80          return
    81        }
    82  
    83        currentHighlight = undefined;
    84  
    85        svgEdges.classed({ active: false })
    86        svgNodes.classed({ active: false })
    87      }
    88  
    89      var svgNode = svgNodes.enter().append("g")
    90        .attr("class", function(node) { return "node " + node.class })
    91        .on("mouseover", highlight)
    92        .on("mouseout", lowlight)
    93  
    94      var nodeLink = svgNode.append("svg:a")
    95        .attr("xlink:href", function(node) { return node.url })
    96        .on("click", function(node) {
    97          var ev = d3.event;
    98          if (ev.defaultPrevented) return; // dragged
    99  
   100          if (ev.ctrlKey || ev.altKey || ev.metaKey || ev.shiftKey) {
   101            return;
   102          }
   103  
   104          if (ev.button != 0) {
   105            return;
   106          }
   107  
   108          ev.preventDefault();
   109  
   110          newUrl.send(node.url);
   111        })
   112  
   113      var jobStatusBackground = nodeLink.append("rect")
   114        .attr("height", function(node) { return node.height() })
   115  
   116  
   117      var animatableBackground = nodeLink.append("foreignObject")
   118        .attr("class", "js-animation-wrapper")
   119        .attr("height", function(node) { return node.height() + (2 * node.animationRadius()) })
   120        .attr("x", function(node) { return -node.animationRadius()})
   121        .attr("y", function(node) { return -node.animationRadius()})
   122  
   123      var animationPadding = animatableBackground.append("xhtml:div")
   124        .style("padding", function(node) {
   125          return node.animationRadius() + "px";
   126        })
   127  
   128      animationPadding.style("height", function(node) { return node.height() + "px" })
   129  
   130      var animationTarget = animationPadding.append("xhtml:div")
   131  
   132      animationTarget.attr("class", "animation")
   133      animationTarget.style("height", function(node) { return node.height() + "px" })
   134  
   135      var pinIconWidth = 6;
   136      var pinIconHeight = 9.75;
   137      nodeLink.filter(function(node) { return node.pinned() }).append("image")
   138          .attr("xlink:href", "/public/images/pin-ic-white.svg")
   139          .attr("width", pinIconWidth)
   140          .attr("y", function(node) { return node.height() / 2 - pinIconHeight / 2 })
   141          .attr("x", function(node) { return node.padding() })
   142  
   143      var iconSize = 12;
   144      nodeLink.filter(function(node) { return node.has_icon() }).append("use")
   145        .attr("xlink:href", function(node) { return "#" + node.id + "-svg-icon" })
   146        .attr("width", iconSize)
   147        .attr("height", iconSize)
   148        .attr("fill", "white")
   149        .attr("y", function(node) { return node.height() / 2 - iconSize / 2 })
   150        .attr("x", function(node) { return node.padding() + (node.pinned() ? pinIconWidth + node.padding() : 0) })
   151  
   152      nodeLink.append("text")
   153        .text(function(node) { return node.name })
   154        .attr("dominant-baseline", "middle")
   155        .attr("text-anchor", function(node) { return node.pinned() || node.has_icon() ? "end" : "middle" })
   156        .attr("x", function(node) { return node.pinned() || node.has_icon() ? node.width() - node.padding() : node.width() / 2 })
   157        .attr("y", function(node) { return node.height() / 2 })
   158  
   159      jobStatusBackground.attr("width", function(node) { return node.width() })
   160      animatableBackground.attr("width", function(node) { return node.width() + (2 * node.animationRadius()) })
   161      animationTarget.style("width", function(node) { return node.width() + "px" })
   162      animationPadding.style("width", function(node) { return node.width() + "px" })
   163  
   164      graph.layout()
   165  
   166      var failureCenters = []
   167      var epsilon = 2
   168      var graphNodes = graph.nodes()
   169      for (var i in graphNodes) {
   170        if (graphNodes[i].status == "failed") {
   171          var xCenter = graphNodes[i].position().x + (graphNodes[i].width() / 2)
   172          var found = false
   173          for (var i in failureCenters) {
   174            if (Math.abs(xCenter - failureCenters[i]) < epsilon) {
   175              found = true
   176              break
   177            }
   178          }
   179          if(!found) {
   180            failureCenters.push(xCenter)
   181          }
   182        }
   183      }
   184  
   185      svg.selectAll("g.fail-triangle-node").remove()
   186      const failTriangleBottom = 20
   187      const failTriangleHeight = 24
   188      for (var i in failureCenters) {
   189        var triangleNode = svg.append("g")
   190          .attr("class", "fail-triangle-node")
   191        var triangleOutline = triangleNode.append("path")
   192          .attr("class", "fail-triangle-outline")
   193          .attr("d", "M191.62,136.3778H179.7521a5,5,0,0,1-4.3309-7.4986l5.9337-10.2851a5,5,0,0,1,8.6619,0l5.9337,10.2851A5,5,0,0,1,191.62,136.3778Z")
   194          .attr("transform", "translate(-174.7446 -116.0927)")
   195        var triangle = triangleNode.append("path")
   196          .attr("class", "fail-triangle")
   197          .attr("d", "M191.4538,133.0821H179.9179a2,2,0,0,1-1.7324-2.9994l5.7679-9.9978a2,2,0,0,1,3.4647,0l5.7679,9.9978A2,2,0,0,1,191.4538,133.0821Z")
   198          .attr("transform", "translate(-174.7446 -116.0927)")
   199        var triangleBBox = triangleNode.node().getBBox()
   200        var triangleScale = failTriangleHeight / triangleBBox.height
   201        var triangleWidth = triangleBBox.width * triangleScale
   202        var triangleX = failureCenters[i] - (triangleWidth / 2)
   203        var triangleY = -failTriangleBottom - failTriangleHeight
   204        triangleNode.attr("transform", "translate(" + triangleX + ", " + triangleY + ") scale(" + triangleScale + ")")
   205      }
   206  
   207      nodeLink.attr("class", function(node) {
   208        var classes = [];
   209  
   210        if (node.debugMarked) {
   211          classes.push("marked");
   212        }
   213  
   214        if (node.columnMarked) {
   215          classes.push("column-marked");
   216        }
   217  
   218        return classes.join(" ");
   219      });
   220  
   221      svgNode.attr("transform", function(node) {
   222        var position = node.position();
   223        return "translate("+position.x+", "+position.y+")"
   224      })
   225  
   226      svgEdge.append("path")
   227        .attr("d", function(edge) { return edge.path() })
   228        .on("mouseover", highlight)
   229        .on("mouseout", lowlight)
   230  
   231      var bbox = svg.node().getBBox();
   232      d3.select(svg.node().parentNode)
   233        .attr("viewBox", "" + (bbox.x - 20) + " " + (bbox.y - 20) + " " + (bbox.width + 40) + " " + (bbox.height + 40))
   234  
   235      const originalJobs = [...document.querySelectorAll(".job")];
   236      const jobAnimations = originalJobs.map(el => el.cloneNode(true));
   237      jobAnimations.forEach(el => {
   238        const foreignObject = el.querySelector('foreignObject');
   239        if (foreignObject != null) {
   240          removeElement(foreignObject);
   241        }
   242        el.classList.remove('job');
   243        el.classList.add('job-animation-node');
   244        el.querySelectorAll('a').forEach(removeElement);
   245        if (foreignObject != null) {
   246          removeElement(foreignObject);
   247        }
   248        el.appendChild(foreignObject);
   249  
   250        el.querySelectorAll('text').forEach(removeElement);
   251      });
   252      originalJobs.forEach(el => 
   253        el.querySelectorAll('.js-animation-wrapper').forEach(removeElement)
   254      );
   255      const canvas = document.querySelector('svg > g');
   256      if (canvas != null) {
   257        canvas.prepend(...jobAnimations);
   258      }
   259  
   260      const largestEdge = Math.max(bbox.width, bbox.height);
   261      const animations = document.querySelectorAll('.animation');
   262      animations.forEach(el => {
   263        if (largestEdge < 500) {
   264          el.classList.add("animation-small");
   265        } else if (largestEdge < 1500) {
   266          el.classList.add("animation-medium");
   267        } else if (largestEdge < 3000) {
   268          el.classList.add("animation-large");
   269        } else {
   270          el.classList.add("animation-xlarge");
   271        }
   272      })
   273  
   274      if (currentHighlight) {
   275        svgNodes.each(function(node) {
   276          if (node.key == currentHighlight) {
   277            highlight(node)
   278          }
   279        });
   280  
   281        svgEdges.each(function(node) {
   282          if (node.key == currentHighlight) {
   283            highlight(node)
   284          }
   285        });
   286      }
   287    }
   288  };
   289  
   290  function removeElement(el) {
   291    if (el == null) {
   292      return;
   293    }
   294    if (el.parentNode == null) {
   295      return;
   296    }
   297    el.parentNode.removeChild(el);
   298  }
   299  
   300  var zoom = (function() {
   301    var z;
   302    return function() {
   303      z = z || d3.behavior.zoom();
   304      return z;
   305    }
   306  })();
   307  
   308  var shouldResetPipelineFocus = false;
   309  
   310  function createPipelineSvg(svg) {
   311    var g = d3.select("g.test")
   312    if (g.empty()) {
   313      svg.append("defs").append("filter")
   314        .attr("id", "embiggen")
   315        .append("feMorphology")
   316        .attr("operator", "dilate")
   317        .attr("radius", "4");
   318  
   319      g = svg.append("g").attr("class", "test")
   320      svg.on("mousedown", function() {
   321        var ev = d3.event;
   322        if (ev.button || ev.ctrlKey)
   323          ev.stopImmediatePropagation();
   324        }).call(zoom().scaleExtent([0.5, 10]).on("zoom", function() {
   325        var ev = d3.event;
   326        if (shouldResetPipelineFocus) {
   327          shouldResetPipelineFocus = false;
   328          resetPipelineFocus();
   329        } else {
   330          g.attr("transform", "translate(" + ev.translate + ") scale(" + ev.scale + ")");
   331        }
   332      }));
   333    }
   334    return g
   335  }
   336  
   337  export function resetPipelineFocus() {
   338    var g = d3.select("g.test");
   339  
   340    if (!g.empty()) {
   341      g.attr("transform", "");
   342      zoom().translate([0,0]).scale(1).center(0,0);
   343    } else {
   344      shouldResetPipelineFocus = true
   345    }
   346  
   347    return g
   348  }
   349  
   350  function createGraph(svg, jobs, resources) {
   351    var graph = new Graph();
   352  
   353    var resourceURLs = {};
   354    var resourceBuild = {};
   355    var resourcePinned = {};
   356    var resourceIcons = {};
   357  
   358    for (var i in resources) {
   359      var resource = resources[i];
   360      resourceURLs[resource.name] = "/teams/"+resource.team_name+"/pipelines/"+resource.pipeline_name+"/resources/"+encodeURIComponent(resource.name);
   361      resourceBuild[resource.name] = resource.build;
   362      resourcePinned[resource.name] = resource.pinned_version;
   363      resourceIcons[resource.name] = resource.icon;
   364    }
   365  
   366    for (var i in jobs) {
   367      var job = jobs[i];
   368  
   369      var id = jobNode(job.name);
   370  
   371      var classes = ["job"];
   372  
   373      var url = "/teams/"+job.team_name+"/pipelines/"+job.pipeline_name+"/jobs/"+encodeURIComponent(job.name);
   374      if (job.next_build) {
   375        var build = job.next_build
   376        url = "/teams/"+build.team_name+"/pipelines/"+build.pipeline_name+"/jobs/"+encodeURIComponent(build.job_name)+"/builds/"+build.name;
   377      } else if (job.finished_build) {
   378        var build = job.finished_build
   379        url = "/teams/"+build.team_name+"/pipelines/"+build.pipeline_name+"/jobs/"+encodeURIComponent(build.job_name)+"/builds/"+build.name;
   380      }
   381  
   382      var status;
   383      if (job.paused) {
   384        status = "paused";
   385      } else if (job.finished_build) {
   386        status = job.finished_build.status
   387      } else {
   388        status = "no-builds";
   389      }
   390  
   391      classes.push(status);
   392  
   393      if (job.next_build) {
   394        classes.push(job.next_build.status);
   395      }
   396  
   397      graph.setNode(id, new GraphNode({
   398        id: id,
   399        name: job.name,
   400        class: classes.join(" "),
   401        status: status,
   402        url: url,
   403        svg: svg,
   404      }));
   405    }
   406  
   407    var resourceStatus = function (resource) {
   408      var status = "";
   409      if (resourceBuild[resource]) {
   410        status += " " + resourceBuild[resource].status;
   411      }
   412      if (resourcePinned[resource]) {
   413        status += " pinned";
   414      }
   415  
   416      return status;
   417    };
   418  
   419    // populate job output nodes and edges
   420    for (var i in jobs) {
   421      var job = jobs[i];
   422      var id = jobNode(job.name);
   423  
   424      for (var j in job.outputs) {
   425        var output = job.outputs[j];
   426  
   427        var outputId = outputNode(job.name, output.resource);
   428  
   429        var jobOutputNode = graph.node(outputId);
   430        if (!jobOutputNode) {
   431          addIcon(resourceIcons[output.resource], outputId);
   432          jobOutputNode = new GraphNode({
   433            id: outputId,
   434            name: output.resource,
   435            icon: resourceIcons[output.resource],
   436            key: output.resource,
   437            class: "resource output" + resourceStatus(output.resource),
   438            repeatable: true,
   439            url: resourceURLs[output.resource],
   440            svg: svg
   441          });
   442  
   443          graph.setNode(outputId, jobOutputNode);
   444        }
   445  
   446        graph.addEdge(id, outputId, output.resource, null)
   447      }
   448    }
   449  
   450    // populate dependant job input edges
   451    //
   452    // do this first as this is what primarily determines node ranks
   453    for (var i in jobs) {
   454      var job = jobs[i];
   455      var id = jobNode(job.name);
   456  
   457      for (var j in job.inputs) {
   458        var input = job.inputs[j];
   459  
   460        if (input.passed && input.passed.length > 0) {
   461          for (var p in input.passed) {
   462            var sourceJobNode = jobNode(input.passed[p]);
   463  
   464            var sourceOutputNode = outputNode(input.passed[p], input.resource);
   465            var sourceInputNode = inputNode(input.passed[p], input.resource);
   466  
   467            var sourceNode;
   468            if (graph.node(sourceOutputNode)) {
   469              sourceNode = sourceOutputNode;
   470            } else {
   471              if (!graph.node(sourceInputNode)) {
   472                addIcon(resourceIcons[input.resource], sourceInputNode);
   473                graph.setNode(sourceInputNode, new GraphNode({
   474                  id: sourceInputNode,
   475                  name: input.resource,
   476                  icon: resourceIcons[input.resource],
   477                  key: input.resource,
   478                  class: "resource constrained-input" + resourceStatus(input.resource),
   479                  repeatable: true,
   480                  url: resourceURLs[input.resource],
   481                  svg: svg
   482                }));
   483              }
   484  
   485              if (graph.node(sourceJobNode)) {
   486                graph.addEdge(sourceJobNode, sourceInputNode, input.resource, null);
   487              }
   488  
   489              sourceNode = sourceInputNode;
   490            }
   491  
   492            graph.addEdge(sourceNode, id, input.resource, {trigger: input.trigger});
   493          }
   494        }
   495      }
   496    }
   497  
   498    // populate unconstrained job inputs
   499    //
   500    // now that we know the rank, draw one unconstrained input per rank
   501    for (var i in jobs) {
   502      var job = jobs[i];
   503      var id = jobNode(job.name);
   504  
   505      var node = graph.node(id);
   506  
   507      for (var j in job.inputs) {
   508        var input = job.inputs[j];
   509        var status = "";
   510  
   511        if (!input.passed || input.passed.length == 0) {
   512          var inputId = inputNode(job.name, input.resource+"-unconstrained");
   513  
   514          if (!graph.node(inputId)) {
   515            addIcon(resourceIcons[input.resource], inputId);
   516            graph.setNode(inputId, new GraphNode({
   517              id: inputId,
   518              name: input.resource,
   519              icon: resourceIcons[input.resource],
   520              key: input.resource,
   521              class: "resource input" + resourceStatus(input.resource),
   522              status: status,
   523              repeatable: true,
   524              url: resourceURLs[input.resource],
   525              svg: svg,
   526              equivalentBy: input.resource+"-unconstrained",
   527            }));
   528          }
   529  
   530          graph.addEdge(inputId, id, input.resource, {trigger: input.trigger})
   531        }
   532      }
   533    }
   534  
   535    graph.computeRanks();
   536    graph.collapseEquivalentNodes();
   537    graph.addSpacingNodes();
   538  
   539    return graph;
   540  }
   541  
   542  export function addIcon(iconName, nodeId) {
   543    iconsModulePromise.then(icons => {
   544      var id = nodeId + "-svg-icon";
   545      if (document.getElementById(id) === null) {
   546        var svg = icons.svg(iconName, id);
   547        var template = document.createElement('template');
   548        template.innerHTML = svg;
   549        var icon = template.content.firstChild;
   550        var iconStore = document.getElementById("icon-store");
   551        if (iconStore == null) {
   552          iconStore = createIconStore();
   553        }
   554        iconStore.appendChild(icon)
   555      }
   556    })
   557  }
   558  
   559  function createIconStore() {
   560    const iconStore = document.createElement('div');
   561    iconStore.id = "icon-store";
   562    iconStore.style.display = "none";
   563    document.body.appendChild(iconStore);
   564    return iconStore;
   565  }
   566  
   567  function groupNode(name) {
   568    return "group-"+name;
   569  }
   570  
   571  function jobNode(name) {
   572    return "job-"+name;
   573  }
   574  
   575  function gatewayNode(jobNames) {
   576    return "gateway-"+jobNames.sort().join("-");
   577  }
   578  
   579  function outputNode(jobName, resourceName) {
   580    return "job-"+jobName+"-output-"+resourceName;
   581  }
   582  
   583  function inputNode(jobName, resourceName) {
   584    return "job-"+jobName+"-input-"+resourceName;
   585  }