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 – 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 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 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(
[