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

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


The following commit(s) were added to refs/heads/main by this push:
     new cd70afdad9 add auto refresh to dags home page (#22900)
cd70afdad9 is described below

commit cd70afdad92ee72d96edcc0448f2eb9b44c8597e
Author: Tal Nagar <[email protected]>
AuthorDate: Sun May 1 13:59:30 2022 +0300

    add auto refresh to dags home page (#22900)
    
    * add auto refresh to dags home page
    
    * fix lint errors
    
    * fix lint
    
    * stop refresh when page is not focused or no active dag runs. change css 
for layout
    
    * remove margin for refresh switch
    
    * Update airflow/www/static/css/main.css
    
    Co-authored-by: Brent Bovenzi <[email protected]>
    
    * Update airflow/www/static/js/dags.js
    
    Co-authored-by: Brent Bovenzi <[email protected]>
    
    * Update airflow/www/static/js/dags.js
    
    Co-authored-by: Brent Bovenzi <[email protected]>
    
    * fix text case
    
    * Update airflow/www/static/js/dags.js
    
    Co-authored-by: Brent Bovenzi <[email protected]>
    
    * add comment on refresh interval
    
    * refactor last dag run handler date update
    
    Co-authored-by: Brent Bovenzi <[email protected]>
---
 airflow/www/static/css/main.css         |   2 +-
 airflow/www/static/js/dags.js           | 132 ++++++++-
 airflow/www/templates/airflow/dags.html | 468 ++++++++++++++++++--------------
 airflow/www/views.py                    |   2 +-
 4 files changed, 390 insertions(+), 214 deletions(-)

diff --git a/airflow/www/static/css/main.css b/airflow/www/static/css/main.css
index 37bee892bd..05eda9d42a 100644
--- a/airflow/www/static/css/main.css
+++ b/airflow/www/static/css/main.css
@@ -422,7 +422,7 @@ label[for="timezone-other"],
 }
 
 .refresh-actions > .switch-label {
-  margin: 0 10px 0 20px;
+  margin: 0 10px;
 }
 
 .loading-dots.refresh-loading {
diff --git a/airflow/www/static/js/dags.js b/airflow/www/static/js/dags.js
index 3ada349fb8..99816c2508 100644
--- a/airflow/www/static/js/dags.js
+++ b/airflow/www/static/js/dags.js
@@ -17,7 +17,8 @@
  * under the License.
  */
 
-/* global document, window, $, d3, STATE_COLOR, isoDateToTimeEl */
+/* global document, window, $, d3, STATE_COLOR, isoDateToTimeEl, 
autoRefreshInterval,
+ localStorage */
 
 import { getMetaValue } from './utils';
 import tiTooltip from './task_instances';
@@ -38,6 +39,10 @@ const dagStatsUrl = getMetaValue('dag_stats_url');
 const taskStatsUrl = getMetaValue('task_stats_url');
 const gridUrl = getMetaValue('grid_url');
 
+// auto refresh interval in milliseconds
+// (x2 the interval in tree/graph view since this page can take longer to 
refresh )
+const refreshIntervalMs = 2000;
+
 $('#tags_filter').select2({
   placeholder: 'Filter DAGs by tag',
   allowClear: true,
@@ -160,6 +165,7 @@ function lastDagRunsHandler(error, json) {
     // Show last run as a link to the graph view
     g.selectAll('a')
       .attr('href', 
`${graphUrl}?dag_id=${encodeURIComponent(dagId)}&execution_date=${encodeURIComponent(executionDate)}`)
+      .html('')
       .insert(isoDateToTimeEl.bind(null, executionDate, { title: false }));
 
     // Only show the tooltip when we have a last run and add the json to a 
custom data- attribute
@@ -297,6 +303,7 @@ function drawTaskStatsForDag(dagId, states) {
     .duration(300)
     .delay((d, i) => i * 50)
     .style('opacity', 1);
+
   d3.select('.js-loading-task-stats').remove();
 
   g.append('text')
@@ -351,7 +358,118 @@ function hideSvgTooltip() {
   $('#svg-tooltip').css('display', 'none');
 }
 
+function refreshDagRunsAndTasks(selector, dagId, states) {
+  d3.select(`svg#${selector}-${dagId.replace(/\./g, '__dot__')}`)
+    .selectAll('circle')
+    .data(states)
+    .attr('stroke-width', (d) => {
+      if (d.count > 0) return strokeWidth;
+      return 1;
+    })
+    .attr('stroke', (d) => {
+      if (d.count > 0) return STATE_COLOR[d.state];
+
+      return 'gainsboro';
+    })
+    .attr('fill', '#fff')
+    .attr('r', diameter / 2)
+    .attr('title', (d) => d.state)
+    .on('mouseover', (d) => {
+      if (d.count > 0) {
+        d3.select(this).transition().duration(400)
+          .attr('fill', '#e2e2e2')
+          .style('stroke-width', strokeWidthHover);
+      }
+    });
+  d3.select(`svg#${selector}-${dagId.replace(/\./g, '__dot__')}`)
+    .selectAll('text')
+    .data(states)
+    .text((d) => {
+      if (d.count > 0) {
+        return d.count;
+      }
+      return '';
+    });
+}
+
+function refreshTaskStateHandler(error, ts) {
+  Object.keys(ts).forEach((dagId) => {
+    const states = ts[dagId];
+    refreshDagRunsAndTasks('task-run', dagId, states);
+  });
+}
+
+let refreshInterval;
+
+function checkActiveRuns(json) {
+  // filter latest dag runs and check if there are still running dags
+  const activeRuns = Object.keys(json).filter((dagId) => {
+    const dagRuns = json[dagId].filter((s) => s.state === 
'running').filter((r) => r.count > 0);
+    return (dagRuns.length > 0);
+  });
+  if (activeRuns.length === 0) {
+    // in case there are no active runs increase the interval for auto refresh
+    $('#auto_refresh').prop('checked', false);
+    clearInterval(refreshInterval);
+  }
+}
+
+function refreshDagRuns(error, json) {
+  checkActiveRuns(json);
+  Object.keys(json).forEach((dagId) => {
+    const states = json[dagId];
+    drawDagStatsForDag(dagId, states);
+    refreshDagRunsAndTasks('dag-run', dagId, states);
+  });
+}
+
+function handleRefresh() {
+  $('#loading-dots').css('display', 'inline-block');
+  d3.json(lastDagRunsUrl)
+    .header('X-CSRFToken', csrfToken)
+    .post(encodedDagIds, lastDagRunsHandler);
+  d3.json(dagStatsUrl)
+    .header('X-CSRFToken', csrfToken)
+    .post(encodedDagIds, refreshDagRuns);
+  d3.json(taskStatsUrl)
+    .header('X-CSRFToken', csrfToken)
+    .post(encodedDagIds, refreshTaskStateHandler);
+  setTimeout(() => {
+    $('#loading-dots').css('display', 'none');
+  }, refreshIntervalMs);
+}
+
+function startOrStopRefresh() {
+  if ($('#auto_refresh').is(':checked')) {
+    refreshInterval = setInterval(() => {
+      handleRefresh();
+    }, autoRefreshInterval * refreshIntervalMs);
+  } else {
+    clearInterval(refreshInterval);
+  }
+}
+
+function initAutoRefresh() {
+  const isDisabled = localStorage.getItem('dagsDisableAutoRefresh');
+  $('#auto_refresh').prop('checked', !(isDisabled));
+  startOrStopRefresh();
+  d3.select('#refresh_button').on('click', () => handleRefresh());
+}
+
+// pause autorefresh when the page is not active
+const handleVisibilityChange = () => {
+  if (document.hidden) {
+    clearInterval(refreshInterval);
+  } else {
+    initAutoRefresh();
+  }
+};
+
+document.addEventListener('visibilitychange', handleVisibilityChange);
+
 $(window).on('load', () => {
+  initAutoRefresh();
+
   $('body').on('mouseover', '.has-svg-tooltip', (e) => {
     const elem = e.target;
     const text = elem.getAttribute('title');
@@ -379,3 +497,15 @@ $('.js-next-run-tooltip').each((i, run) => {
     });
   });
 });
+
+$('#auto_refresh').change(() => {
+  if ($('#auto_refresh').is(':checked')) {
+    // Run an initial refresh before starting interval if manually turned on
+    handleRefresh();
+    localStorage.removeItem('dagsDisableAutoRefresh');
+  } else {
+    localStorage.setItem('dagsDisableAutoRefresh', 'true');
+    $('#loading-dots').css('display', 'none');
+  }
+  startOrStopRefresh();
+});
diff --git a/airflow/www/templates/airflow/dags.html 
b/airflow/www/templates/airflow/dags.html
index ba0e35f8c1..96ce6070b6 100644
--- a/airflow/www/templates/airflow/dags.html
+++ b/airflow/www/templates/airflow/dags.html
@@ -22,19 +22,19 @@
 {% from 'airflow/_messages.html' import show_message %}
 
 {%- macro sortable_column(display_name, attribute_name) -%}
-   {% set curr_ordering_direction = (request.args.get('sorting_direction', 
'desc')) %}
-   {% set new_ordering_direction = ('asc' if (request.args.get('sorting_key') 
!= attribute_name or curr_ordering_direction == 'desc') else 'desc') %}
-   <a href="{{ url_for('Airflow.index',
+  {% set curr_ordering_direction = (request.args.get('sorting_direction', 
'desc')) %}
+  {% set new_ordering_direction = ('asc' if (request.args.get('sorting_key') 
!= attribute_name or curr_ordering_direction == 'desc') else 'desc') %}
+  <a href="{{ url_for('Airflow.index',
                        status=request.args.get('status', 'all'),
                        search=request.args.get('search', None),
                        tags=request.args.get('tags', None),
                        sorting_key=attribute_name,
                        sorting_direction=new_ordering_direction
                        ) }}"
-   class="js-tooltip"
-   role="tooltip"
-   title="Sort by {{ new_ordering_direction }} {{ attribute_name }}."
-   >
+     class="js-tooltip"
+     role="tooltip"
+     title="Sort by {{ new_ordering_direction }} {{ attribute_name }}."
+  >
     {{ display_name }}
 
     <span class="material-icons" aria-hidden="true" 
aria-describedby="sorting-tip-{{ display_name }}">
@@ -91,14 +91,14 @@
   {% endfor %}
   {{ super() }}
   {% if sqlite_warning | default(true) %}
-    {% call show_message(category='warning', dismissible=false)  %}
+    {% call show_message(category='warning', dismissible=false) %}
       Do not use <b>SQLite</b> as metadata DB in production &#8211; it should 
only be used for dev/testing.
       We recommend using Postgres or MySQL.
       <a href={{ get_docs_url("howto/set-up-database.html") }}><b>Click 
here</b></a> for more information.
     {% endcall %}
   {% endif %}
   {% if sequential_executor_warning | default(false) %}
-    {% call show_message(category='warning', dismissible=false)  %}
+    {% call show_message(category='warning', dismissible=false) %}
       Do not use <b>SequentialExecutor</b> in production.
       <a href={{ get_docs_url("executor/index.html") }}><b>Click here</b></a> 
for more information.
     {% endcall %}
@@ -109,14 +109,23 @@
   <h2>{{ page_title }}</h2>
   <div id="main_content">
     <div class="row dags-table-header">
-      <div class="col-md-4 no-x-padding">
+      <div class="col-sm-4 col-md-4 no-x-padding">
         <div class="form-group btn-group">
-          <a href="{{ url_for('Airflow.index', status='all', 
search=request.args.get('search', None), tags=request.args.getlist('tags', 
None)) }}" class="btn {{'btn-primary' if status_filter == 'all' else 
'btn-default'}}" title="Show active and paused DAGs">All <span class="badge">{{ 
"{:,}".format(status_count_all) }}</span></a>
-          <a href="{{ url_for('Airflow.index', status='active', 
search=request.args.get('search', None), tags=request.args.getlist('tags', 
None)) }}" class="btn {{'btn-primary' if status_filter == 'active' else 
'btn-default'}}" title="Show only active DAGs">Active <span class="badge">{{ 
"{:,}".format(status_count_active) }}</span></a>
-          <a href="{{ url_for('Airflow.index', status='paused', 
search=request.args.get('search', None), tags=request.args.getlist('tags', 
None)) }}" class="btn {{'btn-primary' if status_filter == 'paused' else 
'btn-default'}}" title="Show only paused DAGs">Paused <span class="badge">{{ 
"{:,}".format(status_count_paused) }}</span></a>
+          <a
+            href="{{ url_for('Airflow.index', status='all', 
search=request.args.get('search', None), tags=request.args.getlist('tags', 
None)) }}"
+            class="btn {{ 'btn-primary' if status_filter == 'all' else 
'btn-default' }}"
+            title="Show active and paused DAGs">All <span class="badge">{{ 
"{:,}".format(status_count_all) }}</span></a>
+          <a
+            href="{{ url_for('Airflow.index', status='active', 
search=request.args.get('search', None), tags=request.args.getlist('tags', 
None)) }}"
+            class="btn {{ 'btn-primary' if status_filter == 'active' else 
'btn-default' }}"
+            title="Show only active DAGs">Active <span class="badge">{{ 
"{:,}".format(status_count_active) }}</span></a>
+          <a
+            href="{{ url_for('Airflow.index', status='paused', 
search=request.args.get('search', None), tags=request.args.getlist('tags', 
None)) }}"
+            class="btn {{ 'btn-primary' if status_filter == 'paused' else 
'btn-default' }}"
+            title="Show only paused DAGs">Paused <span class="badge">{{ 
"{:,}".format(status_count_paused) }}</span></a>
         </div>
       </div>
-      <div class="col-sm-6 col-md-3">
+      <div class="col-sm-2 col-md-2">
         <form id="tags_form" style="width: 100%; text-align: left;">
           <div class="form-group search-input" style="width:100%;">
             <select multiple name="tags" id="tags_filter" 
class="select2-drop-mask" style="width: 100%;">
@@ -125,225 +134,260 @@
               {% endfor %}
             </select>
             {% if tags_filter|length > 0 %}
-              <button type="reset" aria-label="Clear all tags" class="btn 
btn-default btn-sm material-icons search-input__clear-btn">cancel</button>
+              <button type="reset" aria-label="Clear all tags"
+                      class="btn btn-default btn-sm material-icons 
search-input__clear-btn">cancel
+              </button>
             {% endif %}
           </div>
         </form>
       </div>
-      <div class="col-sm-6 col-md-3 col-md-offset-2 no-x-padding">
+      <div class="col-sm-2 col-md-2 col-md-offset-1 no-x-padding">
         <form id="search_form">
           <div class="form-group search-input" style="width: 100%;">
             <label for="dag_query" class="sr-only">Search DAGs</label>
-            <input type="search" id="dag_query" class="typeahead form-control 
search-input__input" data-provide="typeahead" style="width:100%;" 
value="{{search_query}}" autocomplete="off" placeholder="Search DAGs">
+            <input type="search" id="dag_query" class="typeahead form-control 
search-input__input"
+                   data-provide="typeahead" style="width:100%;" value="{{ 
search_query }}" autocomplete="off"
+                   placeholder="Search DAGs">
             {% if search_query %}
-              <button type="reset" aria-label="Clear DAG Search Term" 
class="btn btn-default btn-sm material-icons 
search-input__clear-btn">cancel</button>
+              <button type="reset" aria-label="Clear DAG Search Term"
+                      class="btn btn-default btn-sm material-icons 
search-input__clear-btn">cancel
+              </button>
             {% endif %}
           </div>
         </form>
       </div>
+      <div class="refresh-actions col-sm-2 col-md-2 no-x-padding">
+        {{ loading_dots(id='loading-dots', classes='refresh-loading') }}
+        <label class="switch-label">
+          <input class="switch-input" id="auto_refresh" type="checkbox">
+          <span class="switch" aria-hidden="true"></span>
+          Auto-refresh
+        </label>
+        <button class="btn btn-default btn-sm" id="refresh_button">
+          <span class="material-icons" aria-hidden="true">refresh</span>
+        </button>
+      </div>
     </div>
-    <div class="dags-table-wrap">
-      <div class="dags-table-body">
-        <table class="table table-striped table-bordered table-hover">
-          <thead>
-            <tr>
-              <th width="12">
-                <span class="material-icons text-muted js-tooltip" title="Use 
this toggle to pause/unpause a DAG. The scheduler won't schedule new tasks 
instances for a paused DAG. Tasks already running at pause time won't be 
affected.">info</span>
-              </th>
-              <th>{{ sortable_column("DAG", "dag_id") }}</th>
-              <th>{{ sortable_column("Owner", "owners") }}</th>
-              <th>Runs
-                <span class="material-icons text-muted js-tooltip" 
aria-hidden="true" title="Status of all previous DAG runs.">info</span>
-              </th>
-              <th>Schedule</th>
-              <th style="width:180px;">Last Run
-                <span class="material-icons text-muted js-tooltip" 
aria-hidden="true" title="Date/Time of the latest Dag Run.">info</span>
-              </th>
-              <th style="width:180px;">{{ sortable_column("Next Run", 
"next_dagrun") }}
-                <span class="material-icons text-muted js-tooltip" 
aria-hidden="true" title="Expected Date/Time of the next Dag Run.">info</span>
-              </th>
-              <th>Recent Tasks
-                <span class="material-icons text-muted js-tooltip" 
aria-hidden="true" title="Status of tasks from all active DAG runs or, if not 
currently active, from most recent run.">info</span>
-              </th>
-              <th class="text-center" style="width:110px;">Actions</th>
-              <th style="width:52px;">Links</th>
-            </tr>
-          </thead>
-          <tbody>
-            {% if dags|length == 0 %}
-              <tr><td colspan="10">No results</td></tr>
-            {% endif %}
-            {% for dag in dags %}
-            {% set dag_is_paused = dag.get_is_paused() %}
-              <tr>
-                <td style="padding-right:0;">
-                  {% if dag.can_edit %}
-                    {% set switch_tooltip = 'Pause/Unpause DAG' %}
-                  {% else %}
-                    {% set switch_tooltip = 'DAG is Paused' if dag_is_paused 
else 'DAG is Active' %}
-                  {% endif %}
-                  <label class="switch-label{{' disabled' if not dag.can_edit 
else ''  }} js-tooltip" title="{{ switch_tooltip }}">
-                    <input class="switch-input"  id="toggle-{{ dag.dag_id }}" 
data-dag-id="{{ dag.dag_id }}" type="checkbox"
-                           {{ " checked" if not dag_is_paused else "" }}
-                           {{ " disabled" if not dag.can_edit else "" }}>
-                    <span class="switch" aria-hidden="true"></span>
-                  </label>
-                </td>
-                <td>
-                  <a href="{{ url_for('Airflow.'+ dag.get_default_view(), 
dag_id=dag.dag_id) }}"
-                     title="{{ dag.description[0:80] + '…' if dag.description 
and dag.description|length > 80 else dag.description|default('', true) }}">
-                    <strong>{{ dag.dag_id }}</strong>
-                  </a>
-                  <div>
-                    {% for tag in dag.tags | sort(attribute='name') %}
-                      <a class="label label-info"
-                         href="?tags={{ tag.name }}"
-                         style="margin: 6px 6px 0 0;">
-                         {{ tag.name }}
-                      </a>
-                    {% endfor %}
-                  </div>
-                </td>
-                <td>
-                  {% for owner in dag.owners.split(",") %}
-                  <a class="label label-default"
-                     href="?search={{ owner | trim }}"
-                     style="margin: 3px 6px 3px 0;">
-                     {{ owner | trim }}
-                  </a>
-                  {% endfor %}
-                </td>
-                <td style="padding:0; width:130px;">
-                  {{ loading_dots(classes='js-loading-dag-stats text-muted') }}
-                  <svg height="10" width="10" id="dag-run-{{ dag.safe_dag_id 
}}" style="display: block;"></svg>
-                </td>
-                <td>
-                  <a class="label label-default schedule" href="{{ 
url_for('DagRunModelView.list') }}?_flt_3_dag_id={{ dag.dag_id }}" 
data-dag-id="{{ dag.dag_id }}">
-                    {{ dag.schedule_interval }}
+  </div>
+  <div class="dags-table-wrap">
+    <div class="dags-table-body">
+      <table class="table table-striped table-bordered table-hover">
+        <thead>
+        <tr>
+          <th width="12">
+            <span class="material-icons text-muted js-tooltip"
+                  title="Use this toggle to pause/unpause a DAG. The scheduler 
won't schedule new tasks instances for a paused DAG. Tasks already running at 
pause time won't be affected.">info</span>
+          </th>
+          <th>{{ sortable_column("DAG", "dag_id") }}</th>
+          <th>{{ sortable_column("Owner", "owners") }}</th>
+          <th>Runs
+            <span class="material-icons text-muted js-tooltip" 
aria-hidden="true"
+                  title="Status of all previous DAG runs.">info</span>
+          </th>
+          <th>Schedule</th>
+          <th style="width:180px;">Last Run
+            <span class="material-icons text-muted js-tooltip" 
aria-hidden="true"
+                  title="Date/Time of the latest Dag Run.">info</span>
+          </th>
+          <th style="width:180px;">{{ sortable_column("Next Run", 
"next_dagrun") }}
+            <span class="material-icons text-muted js-tooltip" 
aria-hidden="true"
+                  title="Expected Date/Time of the next Dag Run.">info</span>
+          </th>
+          <th>Recent Tasks
+            <span class="material-icons text-muted js-tooltip" 
aria-hidden="true"
+                  title="Status of tasks from all active DAG runs or, if not 
currently active, from most recent run.">info</span>
+          </th>
+          <th class="text-center" style="width:110px;">Actions</th>
+          <th style="width:52px;">Links</th>
+        </tr>
+        </thead>
+        <tbody>
+        {% if dags|length == 0 %}
+          <tr>
+            <td colspan="10">No results</td>
+          </tr>
+        {% endif %}
+        {% for dag in dags %}
+          {% set dag_is_paused = dag.get_is_paused() %}
+          <tr>
+            <td style="padding-right:0;">
+              {% if dag.can_edit %}
+                {% set switch_tooltip = 'Pause/Unpause DAG' %}
+              {% else %}
+                {% set switch_tooltip = 'DAG is Paused' if dag_is_paused else 
'DAG is Active' %}
+              {% endif %}
+              <label class="switch-label{{ ' disabled' if not dag.can_edit 
else '' }} js-tooltip"
+                     title="{{ switch_tooltip }}">
+                <input class="switch-input" id="toggle-{{ dag.dag_id }}" 
data-dag-id="{{ dag.dag_id }}" type="checkbox"
+                  {{ " checked" if not dag_is_paused else "" }}
+                  {{ " disabled" if not dag.can_edit else "" }}>
+                <span class="switch" aria-hidden="true"></span>
+              </label>
+            </td>
+            <td>
+              <a href="{{ url_for('Airflow.'+ dag.get_default_view(), 
dag_id=dag.dag_id) }}"
+                 title="{{ dag.description[0:80] + '…' if dag.description and 
dag.description|length > 80 else dag.description|default('', true) }}">
+                <strong>{{ dag.dag_id }}</strong>
+              </a>
+              <div>
+                {% for tag in dag.tags | sort(attribute='name') %}
+                  <a class="label label-info"
+                     href="?tags={{ tag.name }}"
+                     style="margin: 6px 6px 0 0;">
+                    {{ tag.name }}
                   </a>
-                  {% if dag is defined and dag.timetable_description %}
-                      <span class="material-icons text-muted js-tooltip" 
aria-hidden="true" data-original-title="Schedule: {{ 
dag.timetable_description|string }}">info</span>
-                  {% endif %}
-                </td>
-                <td id="last-run-{{ dag.safe_dag_id }}" class="text-nowrap 
latest_dag_run">
-                  {{ loading_dots(classes='js-loading-last-run text-muted') }}
-                  <a></a>
-                  <span
-                    aria-hidden="true"
-                    title=""
-                    class="material-icons text-muted js-tooltip 
js-last-run-tooltip"
-                    data-html="true"
-                    style="display:none"
-                  >
+                {% endfor %}
+              </div>
+            </td>
+            <td>
+              {% for owner in dag.owners.split(",") %}
+                <a class="label label-default"
+                   href="?search={{ owner | trim }}"
+                   style="margin: 3px 6px 3px 0;">
+                  {{ owner | trim }}
+                </a>
+              {% endfor %}
+            </td>
+            <td style="padding:0; width:130px;">
+              {{ loading_dots(classes='js-loading-dag-stats text-muted') }}
+              <svg height="10" width="10" id="dag-run-{{ dag.safe_dag_id }}" 
style="display: block;"></svg>
+            </td>
+            <td>
+              <a class="label label-default schedule"
+                 href="{{ url_for('DagRunModelView.list') }}?_flt_3_dag_id={{ 
dag.dag_id }}"
+                 data-dag-id="{{ dag.dag_id }}">
+                {{ dag.schedule_interval }}
+              </a>
+              {% if dag is defined and dag.timetable_description %}
+                <span class="material-icons text-muted js-tooltip" 
aria-hidden="true"
+                      data-original-title="Schedule: {{ 
dag.timetable_description|string }}">info</span>
+              {% endif %}
+            </td>
+            <td id="last-run-{{ dag.safe_dag_id }}" class="text-nowrap 
latest_dag_run">
+              {{ loading_dots(classes='js-loading-last-run text-muted') }}
+              <a></a>
+              <span
+                aria-hidden="true"
+                title=""
+                class="material-icons text-muted js-tooltip 
js-last-run-tooltip"
+                data-html="true"
+                style="display:none"
+              >
                     info
                   </span>
-                </td>
-                <td class="text-nowrap">
-                  {% if dag.next_dagrun is not none %}
-                    <time datetime="{{ dag.next_dagrun }}">{{ dag.next_dagrun 
}}</time>
-                  {% endif %}
-                  {% if dag.next_dagrun_create_after %}
-                    {# data-nextrun is being used to pass next_dagrun dates to 
js to build the full tooltip #}
-                    <span
-                      aria-hidden="true"
-                      data-nextrun="{{ dag.next_dagrun_create_after }},{{ 
dag.next_dagrun_data_interval_start }},{{ dag.next_dagrun_data_interval_end }}"
-                      title=""
-                      class="material-icons text-muted js-tooltip 
js-next-run-tooltip"
-                      data-html="true"
-                    >
+            </td>
+            <td class="text-nowrap">
+              {% if dag.next_dagrun is not none %}
+                <time datetime="{{ dag.next_dagrun }}">{{ dag.next_dagrun 
}}</time>
+              {% endif %}
+              {% if dag.next_dagrun_create_after %}
+                {# data-nextrun is being used to pass next_dagrun dates to js 
to build the full tooltip #}
+                <span
+                  aria-hidden="true"
+                  data-nextrun="{{ dag.next_dagrun_create_after }},{{ 
dag.next_dagrun_data_interval_start }},{{ dag.next_dagrun_data_interval_end }}"
+                  title=""
+                  class="material-icons text-muted js-tooltip 
js-next-run-tooltip"
+                  data-html="true"
+                >
                       info
                     </span>
-                  {% endif %}
-                </td>
-                <td style="padding:0; width:323px; height:10px;">
-                  {{ loading_dots(classes='js-loading-task-stats text-muted') 
}}
-                  <svg height="10" width="10" id='task-run-{{ dag.safe_dag_id 
}}' style="display: block;"></svg>
-                </td>
-                <td class="text-center">
-                  <div class="btn-group">
-                    {% if dag %}
-                    <div class="dropdown">
-                      <a aria-label="Trigger DAG" class="btn btn-sm 
btn-default btn-icon-only{{ ' disabled' if not dag.can_trigger }} 
trigger-dropdown-btn" data-toggle="dropdown">
-                        <span class="material-icons" 
aria-hidden="true">play_arrow</span>
-                      </a>
-                      <ul class="dropdown-menu trigger-dropdown-menu">
-                        <li>
-                          <form method="POST" action="{{ 
url_for('Airflow.trigger') }}">
-                            <input type="hidden" name="csrf_token" value="{{ 
csrf_token() }}">
-                            <input type="hidden" name="dag_id" value="{{ 
dag.dag_id }}">
-                            <input type="hidden" name="unpause" value="True">
-                            <button type="submit" 
class="dropdown-form-btn">Trigger DAG</button>
-                          </form>
-                        </li>
-                        <li><a href="{{ url_for('Airflow.trigger', 
dag_id=dag.dag_id) }}">Trigger DAG w/ config</a></li>
-                      </ul>
-                    </div>
-                    {% endif %}
-                    {# Use dag_id instead of dag.dag_id, because the DAG might 
not exist in the webserver's DagBag #}
-                    <a href="{{ url_for('Airflow.delete', dag_id=dag.dag_id, 
redirect_url=url_for(request.endpoint)) }}" onclick="return 
confirmDeleteDag(this, '{{ dag.dag_id }}')" title="Delete&nbsp;DAG" 
aria-label="Delete DAG" class="btn btn-sm btn-default btn-icon-only {{ ' 
disabled' if not dag.can_delete }}">
-                      <span class="material-icons text-danger" 
aria-hidden="true">delete_outline</span>
+              {% endif %}
+            </td>
+            <td style="padding:0; width:323px; height:10px;">
+              {{ loading_dots(classes='js-loading-task-stats text-muted') }}
+              <svg height="10" width="10" id='task-run-{{ dag.safe_dag_id }}' 
style="display: block;"></svg>
+            </td>
+            <td class="text-center">
+              <div class="btn-group">
+                {% if dag %}
+                  <div class="dropdown">
+                    <a aria-label="Trigger DAG"
+                       class="btn btn-sm btn-default btn-icon-only{{ ' 
disabled' if not dag.can_trigger }} trigger-dropdown-btn"
+                       data-toggle="dropdown">
+                      <span class="material-icons" 
aria-hidden="true">play_arrow</span>
                     </a>
+                    <ul class="dropdown-menu trigger-dropdown-menu">
+                      <li>
+                        <form method="POST" action="{{ 
url_for('Airflow.trigger') }}">
+                          <input type="hidden" name="csrf_token" value="{{ 
csrf_token() }}">
+                          <input type="hidden" name="dag_id" value="{{ 
dag.dag_id }}">
+                          <input type="hidden" name="unpause" value="True">
+                          <button type="submit" 
class="dropdown-form-btn">Trigger DAG</button>
+                        </form>
+                      </li>
+                      <li><a href="{{ url_for('Airflow.trigger', 
dag_id=dag.dag_id) }}">Trigger DAG w/ config</a></li>
+                    </ul>
                   </div>
-                </td>
-                <td class="dags-table-more">
-                  {% if dag %}
-                    <div class="dags-table-more__menu">
-                      <div class="dags-table-more__links">
-                        <a href="{{ url_for('Airflow.code', dag_id=dag.dag_id) 
}}" class="dags-table-more__link">
-                          <span class="material-icons" 
aria-hidden="true">code</span>
-                          Code
-                        </a>
-                        <a href="{{ url_for('Airflow.dag_details', 
dag_id=dag.dag_id) }}" class="dags-table-more__link">
-                          <span class="material-icons" 
aria-hidden="true">details</span>
-                          Details
-                        </a>
-                        <a href="{{ url_for('Airflow.gantt', 
dag_id=dag.dag_id) }}" class="dags-table-more__link">
-                          <span class="material-icons" 
aria-hidden="true">vertical_distribute</span>
-                          Gantt
-                        </a>
-                        <a href="{{ url_for('Airflow.landing_times', 
dag_id=dag.dag_id) }}" class="dags-table-more__link">
-                          <span class="material-icons" 
aria-hidden="true">flight_land</span>
-                          Landing
-                        </a>
-                        <a href="{{ url_for('Airflow.tries', 
dag_id=dag.dag_id) }}" class="dags-table-more__link">
-                          <span class="material-icons" 
aria-hidden="true">repeat</span>
-                          Tries
-                        </a>
-                        <a href="{{ url_for('Airflow.duration', 
dag_id=dag.dag_id) }}" class="dags-table-more__link">
-                          <span class="material-icons" 
aria-hidden="true">hourglass_bottom</span>
-                          Duration
-                        </a>
-                        <a href="{{ url_for('Airflow.calendar', 
dag_id=dag.dag_id) }}" class="dags-table-more__link">
-                          <span class="material-icons" 
aria-hidden="true">event</span>
-                          Calendar
-                        </a>
-                        <a href="{{ url_for('Airflow.graph', 
dag_id=dag.dag_id) }}" class="dags-table-more__link">
-                          <span class="material-icons" 
aria-hidden="true">account_tree</span>
-                          Graph
-                        </a>
-                        <a href="{{ url_for('Airflow.grid', dag_id=dag.dag_id, 
num_runs=num_runs) }}" class="dags-table-more__link">
-                          <span class="material-icons" 
aria-hidden="true">grid_on</span>
-                          Grid
-                        </a>
-                      </div>
-                      <span class="dags-table-more__toggle"><span 
class="material-icons">more_horiz</span></span>
-                    </div>
-                  {% endif %}
-                </td>
-              </tr>
-            {% endfor %}
-          </tbody>
-        </table>
-      </div>
+                {% endif %}
+                {# Use dag_id instead of dag.dag_id, because the DAG might not 
exist in the webserver's DagBag #}
+                <a href="{{ url_for('Airflow.delete', dag_id=dag.dag_id, 
redirect_url=url_for(request.endpoint)) }}"
+                   onclick="return confirmDeleteDag(this, '{{ dag.dag_id }}')" 
title="Delete&nbsp;DAG"
+                   aria-label="Delete DAG"
+                   class="btn btn-sm btn-default btn-icon-only {{ ' disabled' 
if not dag.can_delete }}">
+                  <span class="material-icons text-danger" 
aria-hidden="true">delete_outline</span>
+                </a>
+              </div>
+            </td>
+            <td class="dags-table-more">
+              {% if dag %}
+                <div class="dags-table-more__menu">
+                  <div class="dags-table-more__links">
+                    <a href="{{ url_for('Airflow.code', dag_id=dag.dag_id) }}" 
class="dags-table-more__link">
+                      <span class="material-icons" 
aria-hidden="true">code</span>
+                      Code
+                    </a>
+                    <a href="{{ url_for('Airflow.dag_details', 
dag_id=dag.dag_id) }}" class="dags-table-more__link">
+                      <span class="material-icons" 
aria-hidden="true">details</span>
+                      Details
+                    </a>
+                    <a href="{{ url_for('Airflow.gantt', dag_id=dag.dag_id) 
}}" class="dags-table-more__link">
+                      <span class="material-icons" 
aria-hidden="true">vertical_distribute</span>
+                      Gantt
+                    </a>
+                    <a href="{{ url_for('Airflow.landing_times', 
dag_id=dag.dag_id) }}" class="dags-table-more__link">
+                      <span class="material-icons" 
aria-hidden="true">flight_land</span>
+                      Landing
+                    </a>
+                    <a href="{{ url_for('Airflow.tries', dag_id=dag.dag_id) 
}}" class="dags-table-more__link">
+                      <span class="material-icons" 
aria-hidden="true">repeat</span>
+                      Tries
+                    </a>
+                    <a href="{{ url_for('Airflow.duration', dag_id=dag.dag_id) 
}}" class="dags-table-more__link">
+                      <span class="material-icons" 
aria-hidden="true">hourglass_bottom</span>
+                      Duration
+                    </a>
+                    <a href="{{ url_for('Airflow.calendar', dag_id=dag.dag_id) 
}}" class="dags-table-more__link">
+                      <span class="material-icons" 
aria-hidden="true">event</span>
+                      Calendar
+                    </a>
+                    <a href="{{ url_for('Airflow.graph', dag_id=dag.dag_id) 
}}" class="dags-table-more__link">
+                      <span class="material-icons" 
aria-hidden="true">account_tree</span>
+                      Graph
+                    </a>
+                    <a href="{{ url_for('Airflow.grid', dag_id=dag.dag_id, 
num_runs=num_runs) }}"
+                       class="dags-table-more__link">
+                      <span class="material-icons" 
aria-hidden="true">grid_on</span>
+                      Grid
+                    </a>
+                  </div>
+                  <span class="dags-table-more__toggle"><span 
class="material-icons">more_horiz</span></span>
+                </div>
+              {% endif %}
+            </td>
+          </tr>
+        {% endfor %}
+        </tbody>
+      </table>
     </div>
-    <div class="row">
-      <div class="col-sm-6">
-        {{paging}}
-      </div>
-      <div class="col-sm-6 text-right">
-        Showing <strong>{{num_dag_from}}-{{num_dag_to}}</strong> of 
<strong>{{num_of_all_dags}}</strong> DAGs
-      </div>
+  </div>
+  <div class="row">
+    <div class="col-sm-6">
+      {{ paging }}
     </div>
+    <div class="col-sm-6 text-right">
+      Showing <strong>{{ num_dag_from }}-{{ num_dag_to }}</strong> of 
<strong>{{ num_of_all_dags }}</strong> DAGs
+    </div>
+  </div>
   </div>
   <div id="svg-tooltip" class="tooltip top" style="position: fixed; display: 
none; opacity: 1; pointer-events: none;">
     <div class="tooltip-arrow"></div>
@@ -357,6 +401,8 @@
   <script src="{{ url_for_asset('dags.js') }}"></script>
   <script>
     const STATE_COLOR = {{ state_color|tojson }};
+    const autoRefreshInterval = {{ auto_refresh_interval }};
+
     // Tests rely on confirmDeleteDag to be in the html
     function confirmDeleteDag(link, dagId) {
       if (confirm(`Are you sure you want to delete '${dagId}' now?\n\
diff --git a/airflow/www/views.py b/airflow/www/views.py
index c33688af8f..8777f4e1c4 100644
--- a/airflow/www/views.py
+++ b/airflow/www/views.py
@@ -881,6 +881,7 @@ class Airflow(AirflowBaseView):
             tags_filter=arg_tags_filter,
             sorting_key=arg_sorting_key,
             sorting_direction=arg_sorting_direction,
+            auto_refresh_interval=conf.getint('webserver', 
'auto_refresh_interval'),
         )
 
     @expose('/dag_stats', methods=['POST'])
@@ -5151,7 +5152,6 @@ class DagDependenciesView(AirflowBaseView):
             nodes.append(self._node_dict(dag_node_id, dag, "dag"))
 
             for dep in dependencies:
-
                 nodes.append(self._node_dict(dep.node_id, dep.dependency_id, 
dep.dependency_type))
                 edges.extend(
                     [

Reply via email to