You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by ry...@apache.org on 2021/03/02 19:06:48 UTC

[airflow] branch master updated: remove inline tree js (#14552)

This is an automated email from the ASF dual-hosted git repository.

ryanahamilton pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/master by this push:
     new 8801a0c  remove inline tree js (#14552)
8801a0c is described below

commit 8801a0cc3b39cf3d2a3e5ef6af004d763bdb0b93
Author: Brent Bovenzi <br...@gmail.com>
AuthorDate: Tue Mar 2 13:06:31 2021 -0600

    remove inline tree js (#14552)
---
 airflow/www/static/js/tree.js           | 390 ++++++++++++++++++++++++++++++++
 airflow/www/templates/airflow/tree.html | 377 +-----------------------------
 airflow/www/webpack.config.js           |   2 +-
 3 files changed, 398 insertions(+), 371 deletions(-)

diff --git a/airflow/www/static/js/tree.js b/airflow/www/static/js/tree.js
new file mode 100644
index 0000000..efc69ae
--- /dev/null
+++ b/airflow/www/static/js/tree.js
@@ -0,0 +1,390 @@
+/* eslint-disable func-names */
+/* eslint-disable no-underscore-dangle */
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/* global treeData, document, window, $, d3, moment, call_modal_dag, call_modal, */
+import { escapeHtml } from './main';
+import tiTooltip from './task_instances';
+import getMetaValue from './meta_value';
+
+function toDateString(ts) {
+  const dt = new Date(ts * 1000);
+  return dt.toISOString();
+}
+
+function isDagRun(d) {
+  return d.run_id !== undefined;
+}
+
+function nodeClass(d) {
+  let sclass = 'node';
+  if (d.children === undefined && d._children === undefined) sclass += ' leaf';
+  else {
+    sclass += ' parent';
+    if (d.children === undefined) sclass += ' collapsed';
+    else sclass += ' expanded';
+  }
+  return sclass;
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+  $('span.status_square').tooltip({ html: true });
+
+  const now = Date.now() / 1000;
+  const devicePixelRatio = window.devicePixelRatio || 1;
+  // JSON.parse is faster for large payloads than an object literal
+  const data = JSON.parse(treeData);
+  const barHeight = 20;
+  const axisHeight = 50;
+  const squareX = parseInt(500 * devicePixelRatio, 10);
+  const squareSize = 10;
+  const squareSpacing = 2;
+  const margin = {
+    top: barHeight / 2 + axisHeight, right: 0, bottom: 0, left: barHeight / 2,
+  };
+  const width = parseInt(960 * devicePixelRatio, 10) - margin.left - margin.right;
+  const barWidth = width * 0.9;
+
+  let i = 0;
+  const duration = 400;
+  let root;
+
+  const tree = d3.layout.tree().nodeSize([0, 25]);
+  const nodes = tree.nodes(data);
+  const nodeobj = {};
+
+  function populateTaskInstanceProperties(node) {
+  // populate task instance properties for display purpose
+    let j;
+    for (j = 0; j < node.instances.length; j += 1) {
+      const dataInstance = data.instances[j];
+      const row = node.instances[j];
+
+      if (row === null) {
+        node.instances[j] = {
+          task_id: node.name,
+          execution_date: dataInstance.execution_date,
+        };
+        continue;
+      }
+
+      const taskInstance = {
+        state: row[0],
+        try_number: row[1],
+        start_ts: row[2],
+        duration: row[3],
+      };
+      node.instances[j] = taskInstance;
+
+      taskInstance.task_id = node.name;
+      taskInstance.operator = node.operator;
+      taskInstance.execution_date = dataInstance.execution_date;
+      taskInstance.external_trigger = dataInstance.external_trigger;
+
+      // compute start_date and end_date if applicable
+      if (taskInstance.start_ts !== null) {
+        taskInstance.start_date = toDateString(taskInstance.start_ts);
+        if (taskInstance.state === 'running') {
+          taskInstance.duration = now - taskInstance.start_ts;
+        } else if (taskInstance.duration !== null) {
+          taskInstance.end_date = toDateString(taskInstance.start_ts + taskInstance.duration);
+        }
+      }
+    }
+  }
+
+  for (i = 0; i < nodes.length; i += 1) {
+    const node = nodes[i];
+    nodeobj[node.name] = node;
+
+    if (node.name === '[DAG]') {
+    // skip synthetic root node since it's doesn't contain actual task instances
+      continue;
+    }
+
+    if (node.start_ts !== undefined) {
+      node.start_date = toDateString(node.start_ts);
+    }
+    if (node.end_ts !== undefined) {
+      node.end_date = toDateString(node.end_ts);
+    }
+    if (node.depends_on_past === undefined) {
+      node.depends_on_past = false;
+    }
+
+    populateTaskInstanceProperties(node);
+  }
+
+  const diagonal = d3.svg.diagonal()
+    .projection((d) => [d.y, d.x]);
+
+  const taskTip = d3.tip()
+    .attr('class', 'tooltip d3-tip')
+    .html((toolTipHtml) => toolTipHtml);
+
+  const svg = d3.select('#tree-svg')
+  // .attr("width", width + margin.left + margin.right)
+    .append('g')
+    .attr('class', 'level')
+    .attr('transform', `translate(${margin.left},${margin.top})`);
+
+  data.x0 = 0;
+  data.y0 = 0;
+
+  const baseNode = nodes.length === 1 ? nodes[0] : nodes[1];
+  const numSquare = baseNode.instances.length;
+
+  const xScale = d3.scale.linear()
+    .range([
+      squareSize / 2,
+      (numSquare * squareSize) + ((numSquare - 1) * squareSpacing) - (squareSize / 2),
+    ]);
+
+  d3.select('#tree-svg')
+    .insert('g')
+    .attr('transform',
+      `translate(${squareX + margin.left}, ${axisHeight})`)
+    .attr('class', 'axis')
+    .call(
+      d3.svg.axis()
+        .scale(xScale)
+        .orient('top')
+      // show a tick every 5 instances
+        .ticks(Math.floor(numSquare / 5) || 1)
+        .tickFormat((d) => {
+          if (!numSquare || (d > 0 && numSquare < 3)) {
+            // don't render ticks when there are no instances or when the ticks would overlap
+            return '';
+          }
+          const tickIndex = d === 1 ? numSquare - 1 : Math.round(d * numSquare);
+          return moment(baseNode.instances[tickIndex].execution_date).format('MMM DD, HH:mm');
+        }),
+    )
+    .selectAll('text')
+    .attr('transform', 'rotate(-30)')
+    .style('text-anchor', 'start')
+    .call(taskTip);
+
+  function update(source) {
+  // Compute the flattened node list. TODO use d3.layout.hierarchy.
+    const nodes = tree.nodes(root);
+
+    const height = Math.max(500, nodes.length * barHeight + margin.top + margin.bottom);
+    const width = squareX
+      + (numSquare * (squareSize + squareSpacing))
+      + margin.left + margin.right + 50;
+    d3.select('#tree-svg').transition()
+      .duration(duration)
+      .attr('height', height)
+      .attr('width', width);
+
+    d3.select(self.frameElement).transition()
+      .duration(duration)
+      .style('height', `${height}px`);
+
+    // Compute the "layout".
+    nodes.forEach((n, i) => {
+      n.x = i * barHeight;
+    });
+
+    // Update the nodes…
+    const node = svg.selectAll('g.node')
+      .data(nodes, (d) => d.id || (d.id = ++i));
+
+    const nodeEnter = node.enter().append('g')
+      .attr('class', nodeClass)
+      .attr('transform', () => `translate(${source.y0},${source.x0})`)
+      .style('opacity', 1e-6);
+
+    nodeEnter.append('circle')
+      .attr('r', (barHeight / 3))
+      .attr('class', 'task')
+      .attr('data-toggle', 'tooltip')
+      .on('mouseover', function (d) {
+        let tt = '';
+        if (d.operator !== undefined) {
+          if (d.operator !== undefined) {
+            tt += `operator: ${escapeHtml(d.operator)}<br>`;
+          }
+
+          tt += `depends_on_past: ${escapeHtml(d.depends_on_past)}<br>`;
+          tt += `upstream: ${escapeHtml(d.num_dep)}<br>`;
+          tt += `retries: ${escapeHtml(d.retries)}<br>`;
+          tt += `owner: ${escapeHtml(d.owner)}<br>`;
+          tt += `start_date: ${escapeHtml(d.start_date)}<br>`;
+          tt += `end_date: ${escapeHtml(d.end_date)}<br>`;
+        }
+        taskTip.direction('e');
+        taskTip.show(tt, this);
+        d3.select(this).transition()
+          .style('stroke-width', 3);
+      })
+      .on('mouseout', function (d) {
+        taskTip.hide(d);
+        d3.select(this).transition()
+          .style('stroke-width', (d) => (isDagRun(d) ? '2' : '1'));
+      })
+      .attr('height', barHeight)
+      .attr('width', (d) => barWidth - d.y)
+      .style('fill', (d) => d.ui_color)
+      .attr('task_id', (d) => d.name)
+      .on('click', toggles);
+
+    const text = nodeEnter.append('text')
+      .attr('dy', 3.5)
+      .attr('dx', barHeight / 2)
+      .text((d) => d.name);
+
+    const isBlur = getMetaValue('blur');
+    if (isBlur === 'True') text.attr('class', 'blur');
+
+    nodeEnter.append('g')
+      .attr('class', 'stateboxes')
+      .attr('transform',
+        (d) => `translate(${squareX - d.y},0)`)
+      .selectAll('rect')
+      .data((d) => d.instances)
+      .enter()
+      .append('rect')
+      .on('click', (d) => {
+        if (d.task_id === undefined) call_modal_dag(d);
+        else if (nodeobj[d.task_id].operator === 'SubDagOperator') {
+          // I'm pretty sure that true is not a valid subdag id, which is what call_modal wants
+          call_modal(
+            d.task_id,
+            d.execution_date,
+            nodeobj[d.task_id].extra_links,
+            d.try_number,
+            true,
+          );
+        } else {
+          call_modal(
+            d.task_id,
+            d.execution_date,
+            nodeobj[d.task_id].extra_links,
+            d.try_number,
+            undefined,
+          );
+        }
+      })
+      .attr('class', (d) => `state ${d.state}`)
+      .attr('data-toggle', 'tooltip')
+      .attr('rx', (d) => (isDagRun(d) ? '5' : '1'))
+      .attr('ry', (d) => (isDagRun(d) ? '5' : '1'))
+      .style('shape-rendering', (d) => (isDagRun(d) ? 'auto' : 'crispEdges'))
+      .style('stroke-width', (d) => (isDagRun(d) ? '2' : '1'))
+      .style('stroke-opacity', (d) => (d.external_trigger ? '0' : '1'))
+      .on('mouseover', function (d) {
+        const tt = tiTooltip(d);
+        taskTip.direction('n');
+        taskTip.show(tt, this);
+        d3.select(this).transition()
+          .style('stroke-width', 3);
+      })
+      .on('mouseout', function (d) {
+        taskTip.hide(d);
+        d3.select(this).transition()
+          .style('stroke-width', (dd) => (isDagRun(dd) ? '2' : '1'));
+      })
+      .attr('x', (d, j) => (j * (squareSize + squareSpacing)))
+      .attr('y', -squareSize / 2)
+      .attr('width', 10)
+      .attr('height', 10);
+
+    // Transition nodes to their new position.
+    nodeEnter.transition()
+      .duration(duration)
+      .attr('transform', (d) => `translate(${d.y},${d.x})`)
+      .style('opacity', 1);
+
+    node.transition()
+      .duration(duration)
+      .attr('class', nodeClass)
+      .attr('transform', (d) => `translate(${d.y},${d.x})`)
+      .style('opacity', 1);
+
+    // Transition exiting nodes to the parent's new position.
+    node.exit().transition()
+      .duration(duration)
+      .attr('transform', () => `translate(${source.y},${source.x})`)
+      .style('opacity', 1e-6)
+      .remove();
+
+    // Update the links…
+    const link = svg.selectAll('path.link')
+      .data(tree.links(nodes), (d) => d.target.id);
+
+    // Enter any new links at the parent's previous position.
+    link.enter().insert('path', 'g')
+      .attr('class', 'link')
+      .attr('d', () => {
+        const o = { x: source.x0, y: source.y0 };
+        return diagonal({ source: o, target: o });
+      })
+      .transition()
+      .duration(duration)
+      .attr('d', diagonal);
+
+    // Transition links to their new position.
+    link.transition()
+      .duration(duration)
+      .attr('d', diagonal);
+
+    // Transition exiting nodes to the parent's new position.
+    link.exit().transition()
+      .duration(duration)
+      .attr('d', () => {
+        const o = { x: source.x, y: source.y };
+        return diagonal({ source: o, target: o });
+      })
+      .remove();
+
+    // Stash the old positions for transition.
+    nodes.forEach((d) => {
+      d.x0 = d.x;
+      d.y0 = d.y;
+    });
+
+    $('#loading').remove();
+  }
+
+  update(root = data);
+
+  function toggles(clicked) {
+  // Collapse nodes with the same task id
+    d3.selectAll(`[task_id='${clicked.name}']`).each((d) => {
+      if (clicked !== d && d.children) {
+        d._children = d.children;
+        d.children = null;
+        update(d);
+      }
+    });
+
+    // Toggle clicked node
+    if (clicked._children) {
+      clicked.children = clicked._children;
+      clicked._children = null;
+    } else {
+      clicked._children = clicked.children;
+      clicked.children = null;
+    }
+    update(clicked);
+  }
+});
diff --git a/airflow/www/templates/airflow/tree.html b/airflow/www/templates/airflow/tree.html
index bd8a3a7..d6ca845 100644
--- a/airflow/www/templates/airflow/tree.html
+++ b/airflow/www/templates/airflow/tree.html
@@ -20,6 +20,11 @@
 {% extends "airflow/dag.html" %}
 {% block page_title %}{{ dag.dag_id }} - Tree - {{ appbuilder.app_name }}{% endblock %}
 
+{% block head_meta %}
+  {{ super() }}
+  <meta name="blur" content="{{ blur }}">
+{% endblock %}
+
 {% block head_css %}
   {{ super() }}
   <link rel="stylesheet" type="text/css" href="{{ url_for_asset('tree.css') }}">
@@ -87,376 +92,8 @@
   {{ super() }}
   <script src="{{ url_for_asset('d3.min.js') }}"></script>
   <script src="{{ url_for_asset('d3-tip.js') }}"></script>
-  <script src="{{ url_for_asset('taskInstances.js') }}"></script>
+  <script src="{{ url_for_asset('tree.js') }}"></script>
   <script>
-    $('span.status_square').tooltip({html: true});
-
-    function ts_to_dtstr(ts) {
-      var dt = new Date(ts * 1000);
-      return dt.toISOString();
-    }
-
-    function is_dag_run(d) {
-      return d.run_id != undefined;
-    }
-
-    var now_ts = Date.now()/1000;
-
-    function populate_taskinstance_properties(node) {
-      // populate task instance properties for display purpose
-      var j;
-      for (j=0; j<node.instances.length; j++) {
-        var dr_instance = data.instances[j];
-        var row = node.instances[j];
-
-        if (row === null) {
-          node.instances[j] = {
-            task_id: node.name,
-            execution_date: dr_instance.execution_date,
-          };
-          continue;
-        }
-
-        var task_instance = {
-          state: row[0],
-          try_number: row[1],
-          start_ts: row[2],
-          duration: row[3],
-        };
-        node.instances[j] = task_instance;
-
-        task_instance.task_id = node.name;
-        task_instance.operator = node.operator;
-        task_instance.execution_date = dr_instance.execution_date;
-        task_instance.external_trigger = dr_instance.external_trigger;
-
-        // compute start_date and end_date if applicable
-        if (task_instance.start_ts !== null) {
-          task_instance.start_date = ts_to_dtstr(task_instance.start_ts);
-          if (task_instance.state === "running") {
-            task_instance.duration = now_ts - task_instance.start_ts;
-          } else if (task_instance.duration !== null) {
-            task_instance.end_date = ts_to_dtstr(task_instance.start_ts + task_instance.duration);
-          }
-        }
-      }
-    }
-
-    var devicePixelRatio = window.devicePixelRatio || 1;
-    // JSON.parse is faster for large payloads than an object literal (because the JSON grammar is simpler!)
-    var data = JSON.parse({{ data|tojson }});
-    var barHeight = 20;
-    var axisHeight = 50;
-    var square_x = parseInt(500 * devicePixelRatio);
-    var square_size = 10;
-    var square_spacing = 2;
-    var margin = {top: barHeight/2 + axisHeight, right: 0, bottom: 0, left: barHeight/2},
-        width = parseInt(960 * devicePixelRatio) - margin.left - margin.right,
-        barWidth = width * 0.9;
-
-    var i = 0,
-        duration = 400,
-        root;
-
-    var tree = d3.layout.tree().nodeSize([0, 25]);
-    var nodes = tree.nodes(data);
-    var nodeobj = {};
-    for (i=0; i<nodes.length; i++) {
-      var node = nodes[i];
-      nodeobj[node.name] = node;
-
-      if (node.name === "[DAG]") {
-        // skip synthetic root node since it's doesn't contain actual task instances
-        continue;
-      }
-
-      if (node.start_ts !== undefined) {
-        node.start_date = ts_to_dtstr(node.start_ts);
-      }
-      if (node.end_ts !== undefined) {
-        node.end_date = ts_to_dtstr(node.end_ts);
-      }
-      if (node.depends_on_past === undefined) {
-        node.depends_on_past = false;
-      }
-
-      populate_taskinstance_properties(node);
-    }
-
-    var diagonal = d3.svg.diagonal()
-        .projection(function(d) { return [d.y, d.x]; });
-
-    const taskTip = d3.tip()
-      .attr('class', 'tooltip d3-tip')
-      .html(function(toolTipHtml) {
-        return toolTipHtml;
-    });
-
-    var svg = d3.select("#tree-svg")
-        //.attr("width", width + margin.left + margin.right)
-      .append("g")
-      .attr("class", "level")
-        .attr("transform", `translate(${margin.left},${margin.top})`);
-
-        data.x0 = 0;
-        data.y0 = 0;
-
-      if (nodes.length == 1)
-        var base_node = nodes[0];
-      else
-        var base_node = nodes[1];
-
-      var num_square = base_node.instances.length;
-
-      var xScale = d3.scale.linear()
-      .range([
-        square_size/2,
-        (num_square * square_size) + ((num_square-1) * square_spacing) - (square_size/2)
-      ]);
-
-      d3.select("#tree-svg")
-      .insert("g")
-      .attr("transform",
-        `translate(${square_x + margin.left}, ${axisHeight})`)
-      .attr("class", "axis").call(
-        d3.svg.axis()
-        .scale(xScale)
-        .orient("top")
-        // show a tick every 5 instances
-        .ticks(Math.floor(num_square / 5) || 1)
-        .tickFormat((d, i) => {
-          if (!num_square || (d > 0 && num_square < 3)) {
-            // don't render ticks when there are no instances or when the ticks would overlap
-            return '';
-          }
-          var tickIndex = d === 1 ? num_square - 1 : Math.round(d * num_square);
-          return moment(base_node.instances[tickIndex].execution_date).format('MMM DD, HH:mm');
-        })
-      )
-      .selectAll("text")
-      .attr("transform", "rotate(-30)")
-      .style("text-anchor", "start").call(taskTip);
-
-      function node_class(d) {
-            var sclass = "node";
-            if (d.children === undefined && d._children === undefined)
-              sclass += " leaf";
-            else {
-              sclass += " parent";
-              if (d.children === undefined)
-                sclass += " collapsed"
-              else
-                sclass += " expanded"
-            }
-            return sclass;
-      }
-
-    update(root = data);
-
-    function update(source) {
-
-      // Compute the flattened node list. TODO use d3.layout.hierarchy.
-      var nodes = tree.nodes(root);
-
-      var height = Math.max(500, nodes.length * barHeight + margin.top + margin.bottom);
-      var width = square_x + (num_square * (square_size + square_spacing)) + margin.left + margin.right + 50;
-      d3.select("#tree-svg").transition()
-          .duration(duration)
-          .attr("height", height)
-          .attr("width", width);
-
-      d3.select(self.frameElement).transition()
-          .duration(duration)
-          .style("height", `${height}px`);
-
-      // Compute the "layout".
-      nodes.forEach(function(n, i) {
-        n.x = i * barHeight;
-      });
-
-      // Update the nodes…
-      var node = svg.selectAll("g.node")
-          .data(nodes, function(d) { return d.id || (d.id = ++i); });
-
-      var nodeEnter = node.enter().append("g")
-      .attr("class", node_class)
-      .attr("transform", function() {
-        return `translate(${source.y0},${source.x0})`;
-      })
-      .style("opacity", 1e-6);
-
-      nodeEnter.append("circle")
-          .attr("r", (barHeight / 3))
-          .attr("class", "task")
-          .attr("data-toggle", "tooltip")
-          .on("mouseover", function(d) {
-            var tt = "";
-            if (d.operator != undefined) {
-              if (d.operator != undefined) {
-                tt += `operator: ${escapeHtml(d.operator)}<br>`;
-              }
-
-              tt += `depends_on_past: ${escapeHtml(d.depends_on_past)}<br>`;
-              tt += `upstream: ${escapeHtml(d.num_dep)}<br>`;
-              tt += `retries: ${escapeHtml(d.retries)}<br>`;
-              tt += `owner: ${escapeHtml(d.owner)}<br>`;
-              tt += `start_date: ${escapeHtml(d.start_date)}<br>`;
-              tt += `end_date: ${escapeHtml(d.end_date)}<br>`;
-            }
-            taskTip.direction('e')
-            taskTip.show(tt, this)
-            d3.select(this).transition()
-             .style('stroke-width', 3)
-          })
-          .on('mouseout', function(d) {
-            taskTip.hide(d)
-            d3.select(this).transition()
-             .style("stroke-width", function(d) {return is_dag_run(d)? "2": "1"})
-          })
-          .attr("height", barHeight)
-          .attr("width", function(d) {return barWidth - d.y;})
-          .style("fill", function(d) {return d.ui_color;})
-          .attr("task_id", function(d){return d.name})
-          .on("click", toggles);
-
-      text = nodeEnter.append("text")
-          .attr("dy", 3.5)
-          .attr("dx", barHeight/2)
-          .text(function(d) { return d.name; });
-      {% if blur %}
-      text.attr("class", "blur");
-      {% endif %}
-
-      nodeEnter.append('g')
-          .attr("class", "stateboxes")
-          .attr("transform",
-            function(d) { return `translate(${square_x - d.y},0)`; })
-          .selectAll("rect").data(function(d) { return d.instances; })
-          .enter()
-          .append('rect')
-          .on("click", function(d){
-            if(d.task_id === undefined)
-                call_modal_dag(d);
-            else if(nodeobj[d.task_id].operator=='SubDagOperator')
-                // I'm pretty sure that true is not a valid subdag id, which is what call_modal wants
-                call_modal(d.task_id, d.execution_date, nodeobj[d.task_id].extra_links, d.try_number, true);
-            else
-                call_modal(d.task_id, d.execution_date, nodeobj[d.task_id].extra_links, d.try_number, undefined);
-          })
-          .attr("class", function(d) {return `state ${d.state}`})
-          .attr("data-toggle", "tooltip")
-          .attr("rx", function(d) {return is_dag_run(d)? "5": "1"})
-          .attr("ry", function(d) {return is_dag_run(d)? "5": "1"})
-          .style("shape-rendering", function(d) {return is_dag_run(d)? "auto": "crispEdges"})
-          .style("stroke-width", function(d) {return is_dag_run(d)? "2": "1"})
-          .style("stroke-opacity", function(d) {return d.external_trigger ? "0": "1"})
-          .on("mouseover", function(d){
-            var tt = tiTooltip(d);
-            taskTip.direction('n');
-            taskTip.show(tt, this);
-            d3.select(this).transition()
-             .style('stroke-width', 3)
-          })
-          .on('mouseout', function(d) {
-            taskTip.hide(d)
-            d3.select(this).transition()
-             .style("stroke-width", function(d) {return is_dag_run(d)? "2": "1"})
-          })
-          .attr('x', function(d, i) {return (i*(square_size+square_spacing));})
-          .attr('y', -square_size/2)
-          .attr('width', 10)
-          .attr('height', 10);
-
-
-      // Transition nodes to their new position.
-      nodeEnter.transition()
-          .duration(duration)
-          .attr("transform", function(d) { return `translate(${d.y},${d.x})`; })
-          .style("opacity", 1);
-
-      node.transition()
-          .duration(duration)
-          .attr("class", node_class)
-          .attr("transform", function(d) { return `translate(${d.y},${d.x})`; })
-          .style("opacity", 1);
-
-      // Transition exiting nodes to the parent's new position.
-      node.exit().transition()
-          .duration(duration)
-          .attr("transform", function() { return `translate(${source.y},${source.x})`; })
-          .style("opacity", 1e-6)
-          .remove();
-
-      // Update the links…
-      var link = svg.selectAll("path.link")
-          .data(tree.links(nodes), function(d) { return d.target.id; });
-
-      // Enter any new links at the parent's previous position.
-      link.enter().insert("path", "g")
-          .attr("class", "link")
-          .attr("d", function() {
-            var o = {x: source.x0, y: source.y0};
-            return diagonal({source: o, target: o});
-          })
-        .transition()
-          .duration(duration)
-          .attr("d", diagonal);
-
-      // Transition links to their new position.
-      link.transition()
-          .duration(duration)
-          .attr("d", diagonal);
-
-      // Transition exiting nodes to the parent's new position.
-      link.exit().transition()
-          .duration(duration)
-          .attr("d", function() {
-            var o = {x: source.x, y: source.y};
-            return diagonal({source: o, target: o});
-          })
-          .remove();
-
-      // Stash the old positions for transition.
-      nodes.forEach(function(d) {
-        d.x0 = d.x;
-        d.y0 = d.y;
-      });
-
-      $('#loading').remove()
-    }
-
-    function toggles(clicked_d) {
-        // Collapse nodes with the same task id
-        d3.selectAll(`[task_id='${clicked_d.name}']`).each(function(d){
-          if(clicked_d != d && d.children) {
-              d._children = d.children;
-              d.children = null;
-            update(d);
-          }
-        });
-
-        // Toggle clicked node
-        if(clicked_d._children) {
-            clicked_d.children = clicked_d._children;
-            clicked_d._children = null;
-        } else {
-            clicked_d._children = clicked_d.children;
-            clicked_d.children = null;
-        }
-        update(clicked_d);
-    }
-    // Toggle children on click.
-    function click(d) {
-      if (d.children || d._children){
-        if (d.children) {
-          d._children = d.children;
-          d.children = null;
-        } else {
-          d.children = d._children;
-          d._children = null;
-        }
-        update(d);
-      }
-    }
+    const treeData = {{ data|tojson }}
   </script>
 {% endblock %}
diff --git a/airflow/www/webpack.config.js b/airflow/www/webpack.config.js
index 3101b30..6742131 100644
--- a/airflow/www/webpack.config.js
+++ b/airflow/www/webpack.config.js
@@ -52,7 +52,7 @@ const config = {
     switch: `${CSS_DIR}/switch.css`,
     taskInstances: `${JS_DIR}/task_instances.js`,
     taskInstance: `${JS_DIR}/task_instance.js`,
-    tree: `${CSS_DIR}/tree.css`,
+    tree: [`${CSS_DIR}/tree.css`, `${JS_DIR}/tree.js`],
     circles: `${JS_DIR}/circles.js`,
     durationChart: `${JS_DIR}/duration_chart.js`,
     trigger: `${JS_DIR}/trigger.js`,