blob: 85b5e435e7f0c8c283392de0abaca47fb54bb071 [file] [log] [blame]
/*!
* 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 document, window, $, d3, STATE_COLOR, isoDateToTimeEl, autoRefreshInterval,
localStorage */
import { getMetaValue } from './utils';
import tiTooltip from './task_instances';
import { approxTimeFromNow, formatDateTime } from './datetime_utils';
import openDatasetModal from './openDatasetModal';
const DAGS_INDEX = getMetaValue('dags_index');
const ENTER_KEY_CODE = 13;
const pausedUrl = getMetaValue('paused_url');
const statusFilter = getMetaValue('status_filter');
const autocompleteUrl = getMetaValue('autocomplete_url');
const graphUrl = getMetaValue('graph_url');
const dagRunUrl = getMetaValue('dag_run_url');
const taskInstanceUrl = getMetaValue('task_instance_url');
const blockedUrl = getMetaValue('blocked_url');
const csrfToken = getMetaValue('csrf_token');
const lastDagRunsUrl = getMetaValue('last_dag_runs_url');
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,
});
$('#tags_filter').on('change', (e) => {
e.preventDefault();
const query = new URLSearchParams(window.location.search);
if (e.val.length) {
if (query.has('tags')) query.delete('tags');
e.val.forEach((value) => {
query.append('tags', value);
});
} else {
query.delete('tags');
query.set('reset_tags', 'reset');
}
if (query.has('page')) query.delete('page');
window.location = `${DAGS_INDEX}?${query.toString()}`;
});
$('#tags_form').on('reset', (e) => {
e.preventDefault();
const query = new URLSearchParams(window.location.search);
query.delete('tags');
if (query.has('page')) query.delete('page');
query.set('reset_tags', 'reset');
window.location = `${DAGS_INDEX}?${query.toString()}`;
});
$('#dag_query').on('keypress', (e) => {
// check for key press on ENTER (key code 13) to trigger the search
if (e.which === ENTER_KEY_CODE) {
const query = new URLSearchParams(window.location.search);
query.set('search', e.target.value.trim());
query.delete('page');
window.location = `${DAGS_INDEX}?${query.toString()}`;
e.preventDefault();
}
});
$('#page_size').on('change', function onPageSizeChange() {
const pSize = $(this).val();
window.location = `${DAGS_INDEX}?page_size=${pSize}`;
});
$.each($('[id^=toggle]'), function toggleId() {
const $input = $(this);
const dagId = $input.data('dag-id');
$input.on('change', () => {
const isPaused = $input.is(':checked');
const url = `${pausedUrl}?is_paused=${isPaused}&dag_id=${encodeURIComponent(dagId)}`;
$input.removeClass('switch-input--error');
// Remove focus on element so the tooltip will go away
$input.trigger('blur');
$.post(url).fail(() => {
setTimeout(() => {
$input.prop('checked', !isPaused);
$input.addClass('switch-input--error');
}, 500);
});
});
});
$('.typeahead').typeahead({
source(query, callback) {
return $.ajax(
autocompleteUrl,
{
data: {
query: encodeURIComponent(query),
status: statusFilter,
},
success: callback,
},
);
},
autoSelect: false,
afterSelect(value) {
const query = new URLSearchParams(window.location.search);
query.set('search', value.name);
if (value.type === 'owner') {
window.location = `${DAGS_INDEX}?${query}`;
}
if (value.type === 'dag') {
window.location = `${gridUrl.replace('__DAG_ID__', value.name)}?${query}`;
}
},
});
$('#search_form').on('reset', () => {
const query = new URLSearchParams(window.location.search);
query.delete('search');
query.delete('page');
window.location = `${DAGS_INDEX}?${query}`;
});
$('#main_content').show(250);
const diameter = 25;
const circleMargin = 4;
const strokeWidth = 2;
const strokeWidthHover = 6;
function blockedHandler(error, json) {
$.each(json, function handleBlock() {
const a = document.querySelector(`[data-dag-id="${this.dag_id}"]`);
a.title = `${this.active_dag_run}/${this.max_active_runs} active dag runs`;
if (this.active_dag_run >= this.max_active_runs) {
a.style.color = '#e43921';
}
});
}
function lastDagRunsHandler(error, json) {
$('.js-loading-last-run').remove();
Object.keys(json).forEach((safeDagId) => {
const dagId = json[safeDagId].dag_id;
const executionDate = json[safeDagId].execution_date;
const g = d3.select(`#last-run-${safeDagId}`);
// 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
g.selectAll('span')
.style('display', null)
.attr('data-lastrun', JSON.stringify(json[safeDagId]));
});
}
// Load data-lastrun attribute data to populate the tooltip on hover
d3.selectAll('.js-last-run-tooltip')
.on('mouseover', function mouseoverLastRun() {
const lastRunData = JSON.parse(d3.select(this).attr('data-lastrun'));
d3.select(this).attr('data-original-title', tiTooltip(lastRunData));
});
function drawDagStatsForDag(dagId, states) {
const g = d3.select(`svg#dag-run-${dagId.replace(/\./g, '__dot__')}`)
.attr('height', diameter + (strokeWidthHover * 2))
.attr('width', '120px')
.selectAll('g')
.data(states)
.enter()
.append('g')
.attr('transform', (d, i) => {
const x = (i * (diameter + circleMargin)) + (diameter / 2 + circleMargin);
const y = (diameter / 2) + strokeWidthHover;
return `translate(${x},${y})`;
});
g.append('svg:a')
.attr('href', (d) => `${dagRunUrl}?_flt_3_dag_id=${dagId}&_flt_3_state=${d.state}`)
.append('circle')
.attr('id', (d) => `run-${dagId.replace(/\./g, '_')}${d.state || 'none'}`)
.attr('class', 'has-svg-tooltip')
.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);
}
})
.on('mouseout', (d) => {
if (d.count > 0) {
d3.select(this).transition().duration(400)
.attr('fill', '#fff')
.style('stroke-width', strokeWidth);
}
})
.style('opacity', 0)
.transition()
.duration(300)
.delay((d, i) => i * 50)
.style('opacity', 1);
d3.select('.js-loading-dag-stats').remove();
g.append('text')
.attr('fill', '#51504f')
.attr('text-anchor', 'middle')
.attr('vertical-align', 'middle')
.attr('font-size', 9)
.attr('y', 3)
.style('pointer-events', 'none')
.text((d) => (d.count > 0 ? d.count : ''));
}
function dagStatsHandler(error, json) {
Object.keys(json).forEach((dagId) => {
const states = json[dagId];
drawDagStatsForDag(dagId, states);
});
}
function drawTaskStatsForDag(dagId, states) {
const g = d3.select(`svg#task-run-${dagId.replace(/\./g, '__dot__')}`)
.attr('height', diameter + (strokeWidthHover * 2))
.attr('width', (states.length * (diameter + circleMargin)) + circleMargin)
.selectAll('g')
.data(states)
.enter()
.append('g')
.attr('transform', (d, i) => {
const x = (i * (diameter + circleMargin)) + (diameter / 2 + circleMargin);
const y = (diameter / 2) + strokeWidthHover;
return `translate(${x},${y})`;
});
g.append('svg:a')
.attr('href', (d) => `${taskInstanceUrl}?_flt_3_dag_id=${dagId}&_flt_3_state=${d.state}`)
.append('circle')
.attr('id', (d) => `task-${dagId.replace(/\./g, '_')}${d.state || 'none'}`)
.attr('class', 'has-svg-tooltip')
.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 || 'none')
.on('mouseover', function mouseOver(d) {
if (d.count > 0) {
d3.select(this).transition().duration(400)
.attr('fill', '#e2e2e2')
.style('stroke-width', strokeWidthHover);
}
})
.on('mouseout', function mouseOut(d) {
if (d.count > 0) {
d3.select(this).transition().duration(400)
.attr('fill', '#fff')
.style('stroke-width', strokeWidth);
}
})
.style('opacity', 0)
.transition()
.duration(300)
.delay((d, i) => i * 50)
.style('opacity', 1);
d3.select('.js-loading-task-stats').remove();
g.append('text')
.attr('fill', '#51504f')
.attr('text-anchor', 'middle')
.attr('vertical-align', 'middle')
.attr('font-size', 9)
.attr('y', 3)
.style('pointer-events', 'none')
.text((d) => (d.count > 0 ? d.count : ''));
}
function taskStatsHandler(error, json) {
Object.keys(json).forEach((dagId) => {
const states = json[dagId];
drawTaskStatsForDag(dagId, states);
});
}
function getDagIds({ activeDagsOnly = false } = {}) {
let dagIds = $('[id^=toggle]');
if (activeDagsOnly) {
dagIds = dagIds.filter(':checked');
}
dagIds = dagIds.map(function () {
return $(this).data('dag-id');
}).get();
return dagIds;
}
function getDagStats() {
const dagIds = getDagIds();
const params = new URLSearchParams();
dagIds.forEach((dagId) => {
params.append('dag_ids', dagId);
});
if (params.has('dag_ids')) {
d3.json(blockedUrl)
.header('X-CSRFToken', csrfToken)
.post(params, blockedHandler);
d3.json(lastDagRunsUrl)
.header('X-CSRFToken', csrfToken)
.post(params, lastDagRunsHandler);
d3.json(dagStatsUrl)
.header('X-CSRFToken', csrfToken)
.post(params, dagStatsHandler);
d3.json(taskStatsUrl)
.header('X-CSRFToken', csrfToken)
.post(params, taskStatsHandler);
} else {
// no dags, hide the loading dots
$('.js-loading-task-stats').remove();
$('.js-loading-dag-stats').remove();
}
}
function showSvgTooltip(text, circ) {
const tip = $('#svg-tooltip');
tip.children('.tooltip-inner').text(text);
const centeringOffset = tip.width() / 2;
tip.css({
display: 'block',
left: `${circ.left + 12.5 - centeringOffset}px`, // 12.5 == half of circle width
top: `${circ.top - 25}px`, // 25 == position above circle
});
}
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(({ state }) => state === 'running' || state === 'queued').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({ activeDagsOnly = false } = {}) {
const dagIds = getDagIds({ activeDagsOnly });
const params = new URLSearchParams();
dagIds.forEach((dagId) => {
params.append('dag_ids', dagId);
});
$('#loading-dots').css('display', 'inline-block');
if (params.has('dag_ids')) {
d3.json(lastDagRunsUrl)
.header('X-CSRFToken', csrfToken)
.post(params, lastDagRunsHandler);
d3.json(dagStatsUrl)
.header('X-CSRFToken', csrfToken)
.post(params, refreshDagRuns);
d3.json(taskStatsUrl)
.header('X-CSRFToken', csrfToken)
.post(params, refreshTaskStateHandler);
}
setTimeout(() => {
$('#loading-dots').css('display', 'none');
}, refreshIntervalMs);
}
function startOrStopRefresh() {
if ($('#auto_refresh').is(':checked')) {
refreshInterval = setInterval(() => {
handleRefresh({ activeDagsOnly: true });
}, 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');
const circ = elem.getBoundingClientRect();
showSvgTooltip(text, circ);
});
$('body').on('mouseout', '.has-svg-tooltip', () => {
hideSvgTooltip();
});
getDagStats();
});
$('.js-next-run-tooltip').each((i, run) => {
$(run).on('mouseover', () => {
$(run).attr('data-original-title', () => {
const nextRunData = $(run).attr('data-nextrun');
const [createAfter, intervalStart, intervalEnd] = nextRunData.split(',');
let newTitle = '';
newTitle += `<strong>Run After:</strong> ${formatDateTime(createAfter)}<br>`;
newTitle += `Next Run: ${approxTimeFromNow(createAfter)}<br><br>`;
newTitle += '<strong>Data Interval</strong><br>';
newTitle += `Start: ${formatDateTime(intervalStart)}<br>`;
newTitle += `End: ${formatDateTime(intervalEnd)}`;
return newTitle;
});
});
});
$('#auto_refresh').change(() => {
if ($('#auto_refresh').is(':checked')) {
// Run an initial refresh before starting interval if manually turned on
handleRefresh({ activeDagsOnly: true });
localStorage.removeItem('dagsDisableAutoRefresh');
} else {
localStorage.setItem('dagsDisableAutoRefresh', 'true');
$('#loading-dots').css('display', 'none');
}
startOrStopRefresh();
});
$('.next-dataset-triggered').on('click', (e) => {
const dagId = $(e.target).data('dag-id');
const summary = $(e.target).data('summary');
if (dagId) openDatasetModal(dagId, summary || '');
});