diff --git a/lib/python3.11/site-packages/luigi/static/visualiser/css/luigi.css b/lib/python3.11/site-packages/luigi/static/visualiser/css/luigi.css new file mode 100644 index 0000000000..83ecf77816 --- /dev/null +++ b/lib/python3.11/site-packages/luigi/static/visualiser/css/luigi.css @@ -0,0 +1,254 @@ +.nodeCircle { + stroke: #fff; + stroke-width: 1.5px; +} +text { + font-size:8pt; +} + +.link { + stroke: #999; + stroke-opacity: .6; +} + +svg { + border:1px solid #DDDDDD; + overflow: inherit; +} + +.taskRow { + word-break:break-all; +} + + +@-webkit-keyframes flash { + 0%, 50%, 100% { + opacity: 1; + } + + 25%, 75% { + opacity: 0.2; + } +} + +@keyframes flash { + 0%, 50%, 100% { + opacity: 1; + } + + 25%, 75% { + opacity: 0.2; + } +} + +.RUNNING { + -webkit-animation-duration: 5s; + animation-duration: 5s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + -webkit-animation-iteration-count: 1; + animation-iteration-count: 1; +} + +.live.map { + width: 100%; + height: 600px; + background: #333; +} + +.live.map text { + font-weight: 300; + font-size: 14px; +} + +.live.map .node rect { + stroke-width: 1.5px; + stroke: #bbb; + fill: #666; +} + +.live.map .status { + height: 100%; + width: 15px; + display: block; + float: left; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + margin-right: 4px; +} + +.live.map .DONE .status { + background-color: #7f7; +} + +.live.map .RUNNING .status { + background-color: #7f7; +} + +.live.map .PENDING .status { + background-color: #FFFF46; +} + +.live.map .ERROR .status { + background-color: #f77; +} + +.live.map .FAILED .status { + background-color: #dd4b39; +} + +.live.map .RUNNING .queue { + color: #f77; +} + +.live.map .DISABLED .status { + background-color: #aaaaaa; +} + +.RUNNING { + -webkit-animation-name: flash; + animation-name: flash; +} + +.live.map .consumers { + margin-right: 2px; +} + +.live.map .consumers, +.live.map .name { + margin-top: 4px; +} + +.live.map .consumers:after { + content: "x"; +} + +.live.map .queue { + display: block; + float: left; + width: 130px; + height: 20px; + font-size: 12px; + margin-top: 2px; +} + +.live.map .node g div { + width: 200px; + height: 40px; + color: #fff; +} + +.live.map .node g div span.consumers { + display: inline-block; + width: 20px; +} + +.live.map .edgeLabel text { + width: 50px; + fill: #fff; +} + +.live.map .edgePath path { + stroke: #999; + stroke-width: 1.5px; + fill: #999; +} + +td.details-control { + cursor: pointer; +} + +span.status-icon { + border-top-left-radius: 2px; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 2px; + display: inline-block; + height: 24px; + width: 24px; + text-align: center; + font-size: 12px; + line-height: 24px; + +} + +#serverSide { + float: right; + margin: 4px; +} + +.infoBar { + min-height: 80px; +} + +#taskTable_filter { + margin-top: 9px; +} + +#loadTaskForm input { + width: 20em; +} + +#workerList .box-tools > div { + display: inline-block; +} + +#workerList .btn-set-workers > span.caret { + margin-left: 4px; +} + +#workerList .box-tools > span.label-unread-worker-messages { + margin-right: 6px; + vertical-align: middle; + font-style: italic; + color: red; +} + +#resourceList i.resources-collapse { + padding-left: 10px; +} + +#clear-task-filter { + margin-left: 20px; + cursor: pointer; +} + +#clear-task-filter:hover { + opacity: 0.9; +} + +#clear-task-filter:active { + box-shadow: inset -2px 3px 1px rgba(0,0,0,0.2); +} + +.sidebar-menu li > a.sidebar-folder { + font-weight: bold; + background-color: #ddd !important; +} + +.sidebar-menu li > a.sidebar-folder:hover { + opacity: 0.9; +} + +.sidebar-menu li > a.sidebar-folder.expanded { + background-color: rgb(0, 166, 90) !important; + color: white !important; +} + +.popover{ + max-width: 100% !important; +} + +.modal-header .close { + margin-left: 10px !important; +} + +.modal-dialog.expanded { + width: 90% !important; + max-width: 90% !important; + height: 90% !important; +} + +.modal-dialog.expanded .modal-content { + height: 100% !important; +} \ No newline at end of file diff --git a/lib/python3.11/site-packages/luigi/static/visualiser/index.html b/lib/python3.11/site-packages/luigi/static/visualiser/index.html new file mode 100644 index 0000000000..31a155acf8 --- /dev/null +++ b/lib/python3.11/site-packages/luigi/static/visualiser/index.html @@ -0,0 +1,593 @@ + + + + Luigi Task Visualiser + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ +
+ + +
+
+ +
+
+
+
+
+ +
+ Pending Tasks + ? +
+
+
+ +
+
+ +
+ Running Tasks + ? +
+
+
+ +
+
+ +
+ Batch Running Tasks + ? +
+
+
+ +
+
+ +
+ Done Tasks + ? +
+
+
+ +
+
+ +
+ Failed Tasks + ? +
+
+
+ +
+
+ +
+ Upstream Failure + ? +
+
+
+ +
+
+ +
+ Disabled Tasks + ? +
+
+
+ +
+
+ +
+ Upstream Disabled + ? +
+
+
+ +
+
+
+
+
+
+
+
+ + + + + + + + + + +
NameDetailsPriorityTimeActions
+ + +
+
+
+
+
+ + +
+
+
+
+ + +
+
+ +
+ + +
+
+
+
+ +
+
+
+

+
Dependency Graph
+
+
+
+
+
+
+
+
+
+
+ +
+ + + + + diff --git a/lib/python3.11/site-packages/luigi/static/visualiser/js/visualiserApp.js b/lib/python3.11/site-packages/luigi/static/visualiser/js/visualiserApp.js new file mode 100644 index 0000000000..cc1e72d7b4 --- /dev/null +++ b/lib/python3.11/site-packages/luigi/static/visualiser/js/visualiserApp.js @@ -0,0 +1,1584 @@ +function visualiserApp(luigi) { + var templates = {}; + var typingTimer = 0; + var dt; // DataTable instantiated in $(document).ready() + var missingCategories = {}; + var currentFilter = { + taskFamily: "", + taskCategory: [], + tableFilter: "" + }; + var taskIcons = { + PENDING: 'pause', + RUNNING: 'play', + BATCH_RUNNING: 'play', + DONE: 'check', + FAILED: 'times', + UPSTREAM_FAILED: 'warning', + DISABLED: 'minus-circle', + UPSTREAM_DISABLED: 'warning' + }; + var VISTYPE_DEFAULT = 'svg'; + + /* + * Updates view of the Visualization type. + */ + function updateVisType(newVisType) { + $('#toggleVisButtons label').removeClass('active'); + var visTypeInput = $('#toggleVisButtons input[value="' + newVisType + '"]'); + visTypeInput.parent().addClass('active'); + visTypeInput.prop('checked', true); + } + + function loadTemplates() { + $("script[type='text/template']").each(function(i, element) { + var name = $(element).attr("name"); + var content = $(element).text(); + templates[name] = content; + }); + } + + function renderTemplate(templateName, dataObject) { + return $("
").html(Mustache.render(templates[templateName], dataObject)); + } + + + function formatTime(dateObject) { + return dateObject.getHours() + ":" + dateObject.getMinutes() + ":" + dateObject.getSeconds(); + } + + function taskToDisplayTask(task) { + var taskName = task.name; + var taskParams = JSON.stringify(task.params); + var displayTime = new Date(Math.floor(task.last_updated*1000)).toLocaleString(); + var time_running = -1; + if (task.status == "RUNNING" && "time_running" in task) { + var current_time = new Date().getTime(); + var minutes_running = Math.round((current_time - task.time_running * 1000) / 1000 / 60); + time_running = task.time_running; + displayTime += " | " + minutes_running + " minutes"; + } + return { + taskId: task.taskId, + encodedTaskId: encodeURIComponent(task.taskId), + taskName: taskName, + taskParams: taskParams, + displayName: task.display_name, + priority: task.priority, + resources: JSON.stringify(task.resources_running || task.resources).replace(/,"/g, ', "'), + displayTime: displayTime, + displayTimestamp: task.last_updated, + timeRunning: time_running, + trackingUrl: task.tracking_url, + status: task.status, + graph: (task.status == "PENDING" || task.status == "RUNNING" || task.status == "DONE"), + error: task.status == "FAILED", + re_enable: task.status == "DISABLED" && task.re_enable_able, + mark_as_done: (task.status == "RUNNING" || task.status == "FAILED" || task.status == "DISABLED"), + statusMessage: task.status_message, + progressPercentage: task.progress_percentage, + acceptsMessages: task.accepts_messages, + workerIdRunning: task.worker_running, + }; + } + + function taskCategoryIcon(category) { + var iconClass; + var iconColor; + switch (category) { + case 'PENDING': + iconClass = 'fa-pause'; + iconColor = 'yellow'; + break; + case 'RUNNING': + iconClass = 'fa-play'; + iconColor = 'aqua'; + break; + case 'BATCH_RUNNING': + iconClass = 'fa-play'; + iconColor = 'purple'; + break; + case 'DONE': + iconClass = 'fa-check'; + iconColor = 'green'; + break; + case 'FAILED': + iconClass = 'fa-times'; + iconColor = 'red'; + break; + case 'DISABLED': + iconClass = 'fa-minus-circle'; + iconColor = 'gray'; + break; + case 'UPSTREAM_FAILED': + iconClass = 'fa-warning'; + iconColor = 'maroon'; + break; + case 'UPSTREAM_DISABLED': + iconClass = 'fa-warning'; + iconColor = 'gray'; + break; + default: + iconClass = 'fa-bug'; + iconColor = 'orange'; + break; + } + return ''; + } + + /** + * Filter table by all activated info boxes. + */ + function filterByCategory(dt, activeBoxes) { + if (activeBoxes === undefined) { + activeBoxes = getActiveBoxes(); + } + currentFilter.taskCategory = activeBoxes; + dt.column(0).search(categoryQuery(activeBoxes), regex=true).draw(); + } + + function categoryQuery(activeBoxes) { + // Searched content will be . + return '\\b(' + activeBoxes.join('|') + ')\\b'; + } + + function getActiveBoxes() { + var infoBoxes = $('.info-box'); + + var activeBoxes = []; + infoBoxes.each(function (i) { + if (infoBoxes[i].dataset.on === 'yes') { + activeBoxes.push(infoBoxes[i].dataset.category); + } + }); + return activeBoxes; + } + + function filterByTaskFamily(taskFamily, dt) { + currentFilter.taskFamily = taskFamily; + if (taskFamily === "") { + dt.column(1).search('').draw(); + } + else { + dt.column(1).search('^' + taskFamily + '$', regex = true).draw(); + } + } + + function toggleInfoBox(infoBox, activate) { + var infoBoxColor = infoBox.dataset.color; + var infoBoxIcon = $(infoBox).find('.info-box-icon'); + var colorClass = 'bg-' + infoBoxColor; + + if ((infoBox.dataset.on === undefined) || (infoBox.dataset.on === 'no') || activate) { + infoBox.dataset.on = 'yes'; + infoBoxIcon.removeClass(colorClass); + $(infoBox).addClass(colorClass); + } + else { + infoBox.dataset.on = 'no'; + $(infoBox).removeClass(colorClass); + infoBoxIcon.addClass(colorClass); + } + } + + function renderSidebar(tasks) { + // tasks is a list of task names + var counts = {}; + $.each(tasks, function(i) { + var name = tasks[i]; + if (counts[name] === undefined) { + counts[name] = 0; + } + counts[name] += 1; + }); + var taskList = []; + $.each(counts, function (name) { + var dotIndex = name.indexOf('.'); + var prefix = 'Others'; + if (dotIndex > 0) { + prefix = name.slice(0, dotIndex); + } + var prefixList = taskList.find(function (pref) { + return pref.name == prefix; + }) + if (prefixList) { + prefixList.tasks.push({name: name, count: counts[name]}); + } else { + prefixList = { + name: prefix, + tasks: [{name: name, count: counts[name]}] + } + taskList.push(prefixList); + } + + }); + taskList.sort(function(a,b){ + if (a.name == 'Others') { + if (b.name == 'Others') { + return 0; + } + return 1; + } else if (b.name == 'Others') { + return -1; + } + return a.name.localeCompare(b.name); + }); + taskList.forEach(function(p){ + p.tasks.sort(function(a,b){ + return a.name.localeCompare(b.name); + }); + }); + return renderTemplate("sidebarTemplate", {"tasks": taskList}); + } + + function selectSidebarItem(item) { + var sidebarItems = $('.sidebar').find('li'); + sidebarItems.each(function (i) { + var item2 = sidebarItems[i]; + if (item2.dataset.task === undefined) { + return; + } + if (item === item2) { + if ($(item2).hasClass('active')) { + // item is active, deselect + $(item2).removeClass('active'); + $(item2).find('.badge').removeClass('bg-green'); + } + else { + // select item + $(item2).addClass('active'); + $(item2).find('.badge').addClass('bg-green'); + } + } + else { + // clear any selection + $(item2).removeClass('active'); + $(item2).find('.badge').removeClass('bg-green'); + } + }); + } + + function renderWarnings() { + return renderTemplate( + "warningsTemplate", + {missingCategories: $.map(missingCategories, function (v, k) {return v;})} + ); + } + + function processWorker(worker) { + worker.encoded_first_task = encodeURIComponent(worker.first_task); + worker.tasks = worker.running.map(taskToDisplayTask); + worker.tasks.sort(function(task1, task2) { return task1.timeRunning - task2.timeRunning; }); + worker.start_time = new Date(worker.started * 1000).toLocaleString(); + worker.active = new Date(worker.last_active * 1000).toLocaleString(); + worker.is_disabled = worker.state === 'disabled'; + return worker; + } + + function renderWorkers(workers) { + return renderTemplate("workerTemplate", {"workerList": workers.map(processWorker)}); + } + + function processResource(resource) { + resource.tasks = resource.running.map(taskToDisplayTask); + resource.percent_used = 100 * resource.num_used / resource.num_total; + if (resource.percent_used >= 100) { + resource.bar_type = 'danger'; + resource.percent_used = 100; + } else if (resource.percent_used > 50) { + resource.bar_type = 'warning'; + } else { + resource.bar_type = 'success'; + } + return resource; + } + + function renderResources(resources) { + return renderTemplate("resourceTemplate", { + "resources": resources.map(processResource).sort(function(r1, r2) { + if (r1.percent_used > r2.percent_used) + return -1; + else if (r1.percent_used < r2.percent_used) + return 1; + else if (r1.num_used > r2.num_used) + return -1; + else if (r1.num_used < r2.num_used) + return 1; + else if (r1.name < r2.name) + return -1; + else if (r1.name > r2.name) + return 1; + else + return 0; + }) + }); + } + + function switchTab(tabId) { + $(".tabButton").parent().removeClass("active"); + $(".tab-pane").removeClass("active"); + $("#" + tabId).addClass("active"); + $(".navbar-nav li").removeClass("active"); + $(".js-nav-link[data-tab=" + tabId + "]").parent().addClass("active"); + updateSidebar(tabId); + } + + function showErrorTrace(data) { + data.error = decodeError(data.error); + if (data.taskParams) { + data.taskParams = Object.entries(data.taskParams).map(([k,v]) => `--${k.replace(/_/g, '-')} ${JSON.stringify(v)}`).join(" "); + } + $("#errorModal").empty().append(renderTemplate("errorTemplate", data)); + $("#errorModal").modal({}); + } + + function minMaxWindow(button) { + + const icon = $(button).find(".icon"); + + const isExpanded = icon.hasClass("fa-minus"); + + if (isExpanded) { + $(".modal-dialog").removeClass("expanded"); + icon.removeClass("fa-minus").addClass("fa-plus"); + } else { + $(".modal-dialog").addClass("expanded"); + icon.removeClass("fa-plus").addClass("fa-minus"); + } + } + + function showStatusMessage(data) { + $("#statusMessageModal").empty().append(renderTemplate("statusMessageTemplate", data)); + $("#statusMessageModal").modal({}); + var refreshInterval = setInterval(function() { + if ($("#statusMessageModal").is(":hidden")) + clearInterval(refreshInterval); + else { + luigi.getTaskStatusMessage(data.taskId, function(data) { + if (data.statusMessage === null) + $("#statusMessageModal pre").hide(); + else { + $("#statusMessageModal pre").html(data.statusMessage).show(); + } + }); + luigi.getTaskProgressPercentage(data.taskId, function(data) { + // show or hide the progress bar container in the message modal + $("#statusMessageModal .progress").toggle(data.progressPercentage !== null); + + // adjust the status of both progress bars (message modal and worker list) + var value = data.progressPercentage || 0; + var progressBars = $('#statusMessageModal .progress-bar, ' + + '.worker-table tbody .taskProgressBar[data-task-id="' + data.taskId + '"]'); + progressBars.attr('aria-valuenow', value) + .text(value + '%') + .css({'width': value + '%'}); + }); + } + }, + 500 + ); + } + + function showSchedulerMessageModal(data) { + var $modal = $("#schedulerMessageModal"); + + $modal.empty().append(renderTemplate("schedulerMessageTemplate", data)); + var $input = $modal.find("#schedulerMessageInput"); + var $send = $modal.find("#schedulerMessageButton"); + var $awaitResponse = $modal.find("#schedulerMessageAwaitResponse"); + var $responseContainer = $modal.find("#schedulerMessageResponse"); + var $responseSpinner = $responseContainer.find("pre > i"); + var $responseContent = $responseContainer.find("pre > div"); + + $input.on("keypress", function($event) { + if (event.keyCode == 13) { + $send.trigger("click"); + $event.preventDefault(); + } + }); + + $send.on("click", function($event) { + var content = $input.val(); + var awaitResponse = $awaitResponse.prop("checked"); + if (content && data.worker) { + if (awaitResponse) { + $responseContainer.show(); + $responseSpinner.show(); + $responseContent.empty(); + luigi.sendSchedulerMessage(data.worker, data.taskId, content, function(messageId) { + var interval = window.setInterval(function() { + luigi.getSchedulerMessageResponse(data.taskId, messageId, function(response) { + if (response != null) { + clearInterval(interval); + $responseSpinner.hide(); + $responseContent.html(response); + } + }); + }, 1000); + }); + $event.stopPropagation(); + } else { + $responseContainer.hide(); + luigi.sendSchedulerMessage(data.worker, data.taskId, content); + } + } + }); + + $modal.on("shown.bs.modal", function() { + $input.focus(); + }); + + $modal.modal({}); + } + + function preProcessGraph(dependencyGraph) { + var extraNodes = []; + var seen = {}; + $.each(dependencyGraph, function(i, node) { + seen[node.taskId] = true; + }); + $.each(dependencyGraph, function(i, node) { + $.each(node.deps, function(j, dep) { + if (!seen[dep]) { + seen[dep] = true; + var paramsStrs = (/\((.*)\)/.exec(dep) || ['', ''])[1].split(', '); + var params = {}; + $.each(paramsStrs, function(i, param) { + if (param !== "") { + var kv = param.split('='); + params[kv[0]] = kv[1]; + } + }); + + extraNodes.push({ + name: (/(\w+)\(/.exec(dep) || [])[1], + taskId: dep, + deps: [], + params: params, + status: "TRUNCATED" + }); + } + }); + }); + return dependencyGraph.concat(extraNodes); + } + + function makeGraphCallback(visType, taskId, paint) { + function depGraphCallbackD3(dependencyGraph) { + $("#searchError").empty(); + $("#searchError").removeClass(); + if(dependencyGraph.length > 0) { + $("#dependencyTitle").text(dependencyGraph[0].display_name); + if(dependencyGraph != '{}'){ + for (var id in dependencyGraph) { + if (dependencyGraph[id].deps.length > 0) { + //console.log(asingInput(dependencyGraph, id)); + dependencyGraph[id].inputQueue = asingInput(dependencyGraph, id); + dependencyGraph[id].inputThroughput = 50; + dependencyGraph[id].count = 5; + dependencyGraph[id].consumers = 1; + }else{ + dependencyGraph[id].inputThroughput = 50; + dependencyGraph[id].count = 5; + dependencyGraph[id].consumers = 1; + } + } + } + } else { + $("#searchError").addClass("alert alert-error"); + $("#searchError").text("Couldn't find task " + taskId); + } + drawGraphETL(dependencyGraph, paint); + bindGraphEvents(); + } + + function depGraphCallback (dependencyGraph) { + $("#graphPlaceholder svg").empty(); + $("#searchError").empty(); + $("#searchError").removeClass(); + if(dependencyGraph.length > 0) { + $("#dependencyTitle").text(dependencyGraph[0].display_name); + var hashBaseObj = URI.parseQuery(location.hash.replace('#', '')); + delete hashBaseObj.taskId; + var hashBase = '#' + URI.buildQuery(hashBaseObj) + '&taskId='; + $("#graphPlaceholder").get(0).graph.updateData(dependencyGraph, hashBase); + $("#graphContainer").show(); + bindGraphEvents(); + } else { + $("#searchError").addClass("alert alert-error"); + $("#searchError").text("Couldn't find task " + taskId); + } + } + + function processedCallback(callback) { + function processed(dependencyGraph) { + return callback(preProcessGraph(dependencyGraph)); + } + return processed; + } + + if (visType == 'd3') { + return processedCallback(depGraphCallbackD3); + } + else { + return processedCallback(depGraphCallback); + } + } + + function processHashChange(paint) { + var hash = decodeURIComponent(location.hash); + // Convert fragment params to object. + var fragmentQuery = URI.parseQuery(location.hash.replace('#', '')); // "http://example.org/#!/foo/bar/baz.html"); + + if (fragmentQuery.tab == "workers") { + switchTab("workerList"); + } else if (fragmentQuery.tab == "resources") { + expandResources(fragmentQuery.resources); + switchTab("resourceList"); + } else if (fragmentQuery.tab == "graph") { + var taskId = fragmentQuery.taskId; + var hideDone = fragmentQuery.hideDone === '1' ? true : false; + + // Populate fields with values from hash. + $('#hideDoneCheckbox').prop('checked', hideDone); + $("#invertCheckbox").prop('checked', fragmentQuery.invert === '1' ? true : false); + $("#js-task-id").val(fragmentQuery.taskId); + + // Empty errors. + $("#searchError").empty(); + $("#searchError").removeClass(); + + var visType = fragmentQuery.visType || VISTYPE_DEFAULT; + if (taskId) { + var depGraphCallback = makeGraphCallback(visType, taskId, paint); + + if (fragmentQuery.invert) { + luigi.getInverseDependencyGraph(taskId, depGraphCallback, !hideDone); + } else { + luigi.getDependencyGraph(taskId, depGraphCallback, !hideDone); + } + } + updateVisType(visType); + initVisualisation(visType); + switchTab("dependencyGraph"); + } else { + // Tasks tab. + + // Populate fields with values from hash. + if (fragmentQuery.length) { + $('select[name=taskTable_length]').val(fragmentQuery.length); + } + $("#serverSideCheckbox").prop('checked', fragmentQuery.filterOnServer === '1' ? true : false); + dt.search(fragmentQuery.search__search); + + $('#familySidebar li').removeClass('active'); + $('#familySidebar li .badge').removeClass('bg-green'); + if (fragmentQuery.family) { + family_item = $('#familySidebar li[data-task="' + fragmentQuery.family + '"]'); + family_item.addClass('active'); + family_item.find('.badge').addClass('bg-green'); + filterByTaskFamily(fragmentQuery.family, dt); + } + + if (fragmentQuery.statuses) { + var statuses = JSON.parse(fragmentQuery.statuses); + $.each(statuses, function (status) { + toggleInfoBox($('#' + statuses[status] + '_info')[0], true); + }); + filterByCategory(dt, statuses); + } + + if (fragmentQuery.order) { + dt.order([fragmentQuery.order.split(',')]); + } + dt.draw(); + switchTab("taskList"); + } + } + + function bindGraphEvents() { + var fragmentQuery = URI.parseQuery(location.hash.replace('#', '')); + var visType = fragmentQuery.visType; + if (visType === 'd3') { + $('.node').click(function(event) { + var taskDiv = $(this).find('.taskNode'); + var taskId = taskDiv.attr("data-task-id"); + event.preventDefault(); + // NOTE : hasClass() not reliable inside SVG + if ($(this).attr('class').match(/\bFAILED\b/)) { + luigi.getErrorTrace(taskId, function (error) { + showErrorTrace(error); + }); + } + else { + fragmentQuery['taskId'] = taskId; + window.location.href = 'index.html#' + URI.buildQuery(fragmentQuery); + } + }); + } + else { + $(".graph-node-a").click(function(event) { + var taskId = $(this).attr("data-task-id"); + var status = $(this).attr("data-task-status"); + if (status == "FAILED") { + event.preventDefault(); + luigi.getErrorTrace(taskId, function(error) { + showErrorTrace(error); + }); + } + }); + } + } + + function bindListEvents() { + $(window).on('hashchange', processHashChange); + + $('#serverSideCheckbox').click(function(e) { + e.preventDefault(); + changeState('filterOnServer', this.checked ? '1' : null); + updateTasks(); + }); + + $("#invertCheckbox").click(function(e) { + e.preventDefault(); + changeState('invert', this.checked ? '1' : null); + }); + + $('#hideDoneCheckbox').click(function(e) { + // Copy checkbox value to hash. + e.preventDefault(); + changeState('hideDone', this.checked ? '1' : null); + }); + $("a[href=#list]").click(function() { location.hash=""; }); + $("#loadTaskForm").submit(function(event) { + event.preventDefault(); + var taskId = $(this).find("input").val(); + changeState('taskId', taskId.length > 0 ? taskId : null); + }); + + $('.info-box').on('click', function () { + toggleInfoBox(this); + filterByCategory(dt); + }); + + $('input[name=vis-type]').on('change', function () { + changeState('visType', $(this).val()); + }); + + /* + Note: The #filter-input element is used by LuigiAPI to constrain requests to the server. + When the accompanying button is pressed we force a reload. + */ + $('#serverSide').on('change', 'label', function () { + updateTasks(); + }); + } + + function asingInput(worker, id){ + if (worker[id].deps.length > 0) { + //console.log(worker[id].deps); + return worker[id].deps; + } + } + + function getDurations(tasks, listId){ + var durations = {}; + for (var i = 0; i < listId.length; i++) { + for (var j = 0; j < tasks.length; j++) { + if (listId[i] === tasks[j].taskId) { + // The duration of the task from when it started running to when it finished. + var finishTime = new Date(tasks[j].last_updated*1000); + var startTime = new Date(tasks[j].time_running*1000); + durations[listId[i]] = new Date(finishTime - startTime); + } + } + } + return durations; + } + + function getParam(tasks, id){ + for (var i = 0; i < tasks.length; i++) { + if (tasks[i].taskId === id) { + return tasks[i].worker_running; + } + } + } + + function getStatusTasks(tasks){ + var status; + for (var i = 0; i < tasks.length; i++) { + if (tasks[i].status === "DONE") { + status = true; + } else { + return false; + } + } + return status; + } + + function drawGraphETL(tasks, paint){ + // Set up zoom support + var svg = d3.select("#mysvg"); + var inner = svg.select("g"), + zoom = d3.behavior.zoom().on("zoom", function() { + inner.attr("transform", "translate(" + d3.event.translate + ")" + + "scale(" + d3.event.scale + ")"); + }); + svg.call(zoom); + + // Create map of taskId to task + var taskIdMap = {}; + $.each(tasks, function (i, task) { + taskIdMap[task.taskId] = task; + }); + + var render = new dagreD3.render(); + // Left-to-right layout + var g = new dagreD3.graphlib.Graph(); + g.setGraph({ + nodesep: 70, + ranksep: 50, + rankdir: "LR", + marginx: 20, + marginy: 20, + height: 400, + ranker: "longest-path" + }); + + function draw(isUpdate) { + for (var id in tasks) { + var task = tasks[id]; + var className = task.status; + + var html = "
"; + html += ""; + html += ""+task.name+""; + html += ""+ task.status +""; + html += "
"; + g.setNode(task.taskId, { + labelType: "html", + label: html, + rx: 5, + ry: 5, + padding: 0, + class: className + }); + if (task.inputQueue) { + for (var i = 0; i < task.inputQueue.length; i++) { + // Destination node may not be in tasks if this is an inverted graph + if (taskIdMap[task.inputQueue[i]] !== undefined) { + if (task.status === "DONE") { + var durations = getDurations(tasks, task.inputQueue); + var duration = durations[task.inputQueue[i]]; + var oneDayInMilliseconds = 24 * 60 * 60 * 1000; + var durationLabel; + if (duration.getTime() < oneDayInMilliseconds) { + // Label task duration in stripped ISO format (hh:mm:ss.f) + durationLabel = duration.toISOString().substr(11, 12); + } else { + durationLabel = "> 24h"; + } + g.setEdge(task.inputQueue[i], task.taskId, { + label: durationLabel, + width: 40 + }); + } else { + g.setEdge(task.inputQueue[i], task.taskId, { + width: 40 + }); + } + } + } + + } + } + var styleTooltip = function(name, description) { + return "

" + name + "

" + description + "

"; + }; + inner.call(render, g); + if(paint){ + // Zoom and scale to fit + var zoomScale = zoom.scale(); + var graphWidth = g.graph().width + 80; + var graphHeight = g.graph().height + 40; + var width = parseInt(svg.style("width").replace(/px/, "")); + var height = parseInt(svg.style("height").replace(/px/, "")); + zoomScale = Math.min(width / graphWidth, height / graphHeight); + var translate = [(width/2) - ((graphWidth*zoomScale)/2), (height/2) - ((graphHeight*zoomScale)/2)]; + zoom.translate(translate); + zoom.scale(zoomScale); + zoom.event(isUpdate ? svg.transition().duration(3000) : d3.select("#mysvg")); + } + + inner.selectAll("g.node") + .attr("title", function(v) { return styleTooltip(v, getParam(tasks, v)); }) + .each(function(v) { $(this).tipsy({ gravity: "w", opacity: 1, html: true }); }); + } + draw(); + } + + /* + DataTables functions + */ + // Remove tasks of a given category and add new ones. + function updateTaskCategory(dt, category, tasks) { + var taskMap = {}; + + var mostImportantCategory = function (cat1, cat2) { + var priorities = [ + 'RUNNING', + 'BATCH_RUNNING', + 'DONE', + 'PENDING', + 'UPSTREAM_DISABLED', + 'UPSTREAM_FAILED', + 'DISABLED', + 'FAILED' + ]; + // NOTE : -1 indicates not in list + var i1 = priorities.indexOf(cat1); + var i2 = priorities.indexOf(cat2); + var ret; + if (i1 > i2) { + ret = cat1; + } + else { + ret = cat2; + } + return ret; + }; + + dt.rows(function (i, data) { + taskMap[data.taskId] = data.category; + return data.category === category; + }).remove(); + + var taskCount; + /* Check for integers in tasks. This indicates max-shown-tasks was exceeded */ + if (tasks.length === 1 && typeof(tasks[0]) === 'number') { + taskCount = tasks[0] === -1 ? 'unknown' : tasks[0]; + missingCategories[category] = {name: category, count: taskCount}; + } + else { + var displayTasks = tasks.map(taskToDisplayTask); + displayTasks = displayTasks.filter(function (obj) { + if (obj === null) { + return false; + } + if (category === mostImportantCategory(category, taskMap[obj.taskId])) { + obj.category = category; + return true; + } + return false; + }); + dt.rows.add(displayTasks); + taskCount = displayTasks.length; + delete missingCategories[category]; + } + + $('#'+category+'_info').find('.info-box-number').html(taskCount); + $('#'+category+'_info i.fa').removeClass().addClass('fa fa-'+taskIcons[category]); + } + + function updateCurrentFilter() { + var content; + currentFilter.tableFilter = dt.search(); + + if ((currentFilter.tableFilter === "") && + ($.isEmptyObject(currentFilter.taskCategory)) && + (currentFilter.taskFamily === "")) { + + content = ''; + } + else { + if (currentFilter.taskCategory !== "") { + currentFilter.catNames = $.map(currentFilter.taskCategory, function (x) { + return {name: x}; + }); + } + + content = renderTemplate('currentFilterTemplate', currentFilter); + } + + $('#currentFilter').html(content); + } + + function initVisualisation(newVisType) { + + // Prepare graphPlaceholder for D3 code + if (newVisType == 'd3') { + $('#graphPlaceholder').empty(); + $('#graphPlaceholder').html('
'); + } + else { + $('#graphPlaceholder').empty(); + var graph = new Graph.DependencyGraph($("#graphPlaceholder")[0]); + $("#graphPlaceholder")[0].graph = graph; + } + } + + function updateTasks() { + $('.status-info .info-box-number').text('?'); + $('.status-info i.fa').removeClass().addClass('fa fa-spinner fa-pulse'); + + var ajax1 = luigi.getRunningTaskList(function(runningTasks) { + updateTaskCategory(dt, 'RUNNING', runningTasks); + }); + + var ajax2 = luigi.getBatchRunningTaskList(function(batchRunningTasks) { + updateTaskCategory(dt, 'BATCH_RUNNING', batchRunningTasks); + }); + + var ajax3 = luigi.getFailedTaskList(function(failedTasks) { + updateTaskCategory(dt, 'FAILED', failedTasks); + }); + + var ajax4 = luigi.getUpstreamFailedTaskList(function(upstreamFailedTasks) { + updateTaskCategory(dt, 'UPSTREAM_FAILED', upstreamFailedTasks); + }); + + var ajax5 = luigi.getDisabledTaskList(function(disabledTasks) { + updateTaskCategory(dt, 'DISABLED', disabledTasks); + }); + + var ajax6 = luigi.getUpstreamDisabledTaskList(function(upstreamDisabledTasks) { + updateTaskCategory(dt, 'UPSTREAM_DISABLED', upstreamDisabledTasks); + }); + + var ajax7 = luigi.getPendingTaskList(function(pendingTasks) { + updateTaskCategory(dt, 'PENDING', pendingTasks); + }); + + var ajax8 = luigi.getDoneTaskList(function(doneTasks) { + updateTaskCategory(dt, 'DONE', doneTasks); + }); + + $.when(ajax1, ajax2, ajax3, ajax4, ajax5, ajax6, ajax7, ajax8).done(function () { + dt.draw(); + + $('.sidebar').html(renderSidebar(dt.column(1).data())); + var selectedFamily = $('.sidebar-menu').find('li[data-task="' + currentFilter.taskFamily + '"]')[0]; + selectSidebarItem(selectedFamily); + + if (selectedFamily) { + var selectedUl = $(selectedFamily).parent(); + selectedUl.show(); + selectedUl.prev().addClass('expanded'); + } else { + var others = $('.sidebar-folder:contains(Others)') + others.addClass('expanded') + others.next().show() + } + + $('.sidebar-menu').on('click', 'li:not(.sidebar-folder)', function (e) { + e.stopPropagation(); + if (this.dataset.task) { + selectSidebarItem(this); + if ($(this).hasClass('active')) { + filterByTaskFamily(this.dataset.task, dt); + } + else { + filterByTaskFamily("", dt); + } + } + }); + + $('.sidebar-menu').on('click', '.sidebar-folder', function () { + const ul = this.nextElementSibling; + $(ul).slideToggle() + this.classList.toggle('expanded') + }) + + $('#clear-task-filter').on('click', function () { + filterByTaskFamily("", dt); + }); + + if ($.isEmptyObject(missingCategories)) { + $('#warnings').html(''); + } + else { + $('#warnings').html(renderWarnings()); + } + + processHashChange(); + }); + } + + function updateSidebar(tabName) { + if (tabName === 'taskList') { + $('body').removeClass('sidebar-collapse'); + } + else { + $('body').addClass('sidebar-collapse'); + } + } + + // Error strings may or may not be JSON encoded, depending on client version + // Decoding an unencoded string may raise an exception. + function decodeError(error) { + var decoded; + try { + decoded = JSON.parse(error); + } + catch (e) { + decoded = error; + } + return decoded; + } + + /** + * Return HTML of a task parameter dictionary + * @param params: task parameter dictionary + */ + function renderParams(params) { + var htmls = []; + for (var key in params) { + htmls.push('' + escapeHtml(key) + + '=' + escapeHtml(params[key]) + ''); + } + return htmls.join(', '); + } + + /** + * Updates the number of worker processes of a worker + * @param worker: the id of the worker + * @param n: the number of processes to set + */ + function updateWorkerProcesses(worker, n) { + n = Math.max(1, n); + + // the spinner is just for visual feedback + var $label = $('#workerList').find('#label-n-workers[data-worker="' + worker + '"]'); + $label.html(''); + + luigi.setWorkerProcesses(worker, n, function() { + $label.text(n); + }); + } + + /** + * Updates the number of units of a given resource available in the scheduler + * @param resource: the name of the resource + * @param n: the number of units to set the resource limit to + */ + function updateResourceCount(resource, n) { + var progressBar = $('#' + resource + '-resource-box .progress-bar'); + var used = /(\S+)\//.exec(progressBar.text())[1]; + nVal = parseInt(n); + if (isNaN(nVal) || nVal < 0) { + return; + } + usedVal = parseInt(used); + width = Math.floor(100 * usedVal / nVal); + if (width < 0) { + width = 0; + } + if (width > 100) { + width = 100; + } + luigi.updateResource(resource, n, function() { + progressBar.text(usedVal + '/' + nVal); + progressBar.attr('style', 'width: ' + width + '%'); + }); + } + + /** + * Returns the current units of a resource used + * @param resource: the name of the resource + */ + function currentResourceCount(resource) { + var progressBar = $('#' + resource + '-resource-box .progress-bar'); + var count = /\/(\S+)/.exec(progressBar.text())[1]; + return parseInt(count); + } + + function changeState(key, value) { + var fragmentQuery = URI.parseQuery(location.hash.replace('#', '')); + if (value) { + fragmentQuery[key] = value; + } else { + delete fragmentQuery[key]; + } + location.hash = '#' + URI.buildQuery(fragmentQuery); + } + + function expandedResources() { + return $('.resource-box.in').toArray().map(function (val) { return val.dataset.resource; }); + } + + function expandResources(resources) { + if (resources === undefined) { + resources = []; + } else { + resources = JSON.parse(resources); + } + $('.resource-box').each(function (i, item) { + if (resources.indexOf(item.dataset.resource) === -1) { + $(item).collapse('hide'); + } else { + $(item).collapse('show'); + } + }); + } + + /** + * Create the pause/unpause toggle + */ + function createPauseToggle(checked) { + var check = checked ? " checked" : ""; + var html = $(''); + $('#pause-form').append(html); + $('#pause').bootstrapToggle({ + on: 'Running', + off: 'Paused', + onstyle: 'success', + offstyle: 'danger' + }); + $('#pause').change(function() { + if (this.checked) { + luigi.unpause(); + } else { + luigi.pause(); + } + }) + } + + $(document).ready(function() { + loadTemplates(); + + luigi.hasTaskHistory(function(hasTaskHistory) { + if (hasTaskHistory) { + $('#topNavbar').append(renderTemplate('topNavbarItem', { + label: "History", + href: "../../history", + }).children()[0]); + } + }); + + luigi.isPauseEnabled(function(enabled) { + if (enabled) { + luigi.isPaused(createPauseToggle); + } + }); + + luigi.getWorkerList(function(workers) { + $("#workerList").append(renderWorkers(workers)); + + $('.worker-table tbody').on('click', 'td .statusMessage', function() { + var data = $(this).data(); + showStatusMessage(data); + }); + + $('.worker-table tbody').on('click', 'td .schedulerMessage', function() { + var data = $(this).data(); + showSchedulerMessageModal(data); + }); + }); + + luigi.getResourceList(function(resources) { + $("#resourceList").append(renderResources(resources)); + expandResources(URI.parseQuery(location.hash.replace('#', '')).resources); + $('.resources-collapse').click(function (e) { + e.preventDefault(); + var collapse_block = $(this.dataset.target); + if (collapse_block.hasClass('collapsing')) { + return; + } + var resource = collapse_block.attr('data-resource'); + var resourceList = expandedResources(); + var resourceIdx = resourceList.indexOf(resource); + if (resourceIdx === -1) { + resourceList.push(resource); + } else { + resourceList.splice(resourceIdx, 1); + } + changeState('resources', resourceList.length > 0 ? JSON.stringify(resourceList) : null); + collapse_block.collapse('toggle'); + }); + }); + + dt = $('#taskTable').DataTable({ + stateSave: true, + stateSaveCallback: function(settings, data) { + // Save data table state to browser's hash. + var state = URI.parseQuery(location.hash.replace('#', '')); + + if (data.search.search) { + state.search__search = data.search.search; + } else { + delete state.search__search; + } + + var family_search = data.columns[1].search.search; + if (family_search) { + state.family = family_search.substring(1, family_search.length - 1); + } else { + delete state.family; + } + + if (currentFilter.taskCategory.length > 0) { + state.statuses = JSON.stringify(currentFilter.taskCategory); + } else { + delete state.statuses; + } + + if (data.order && data.order.length) { + state.order = '' + data.order[0][0] + ',' + data.order[0][1]; + } + + if (data.length && data.length !== 10) { + // Keep in hash only if length is not default. + state.length = data.length; + } else { + delete state.length; + } + + if (state.filterOnServer) { + state.filterOnServer = '1'; + } + location.hash = '#' + URI.buildQuery(state); + }, + stateLoadCallback: function(settings) { + // Restore datatable state from browser's hash. + var fragmentQuery = URI.parseQuery(location.hash.replace('#', '')); + + var order = []; + if (fragmentQuery.order) { + order = [fragmentQuery.order.split(',')]; + } + + var family_search = {}; + if (fragmentQuery.family) { + family_search = {'search': '^' + fragmentQuery.family + '$', 'regex': true}; + } + + var status_search = {}; + if (fragmentQuery.statuses) { + var statuses = JSON.parse(fragmentQuery.statuses); + currentFilter.taskCategory = statuses; + status_search = {'search': categoryQuery(statuses), 'regex': true}; + } + + // Prepare state for datatable. + var o = { + order: order, // Table rows order. + length: fragmentQuery.length, // Entries on page. + start: 0, // Pagination initial page. + time: new Date().getTime(), // Current time to help datatable.js to handle asynchronous. + columns: [ + {visible: true, search: status_search}, + {visible: true, search: family_search}, // Name column + {visible: true, search: {}}, // Details column + {visible: true, search: {}}, // Priority column + {visible: true, search: {}}, // Time column + {visible: true, search: {}} // Actions column + ], + // Search input state. + search: { + caseInsensitive: true, + search: fragmentQuery.search__search + } + }; + + return o; + }, + dom: 'l<"#serverSide">frtip', + language: { + search: 'Filter table:' + }, + columns: [ + { + data: 'category', + render: function (data, type, row) { + return taskCategoryIcon(data) + ' ' + data; + } + }, + {data: 'taskName'}, + { + data: 'taskParams', + render: function(data, type, row) { + var params = JSON.parse(data); + if (row.resources !== '{}') { + return '
' + renderParams(params) + '
' + row.resources + '
'; + } else { + return '
' + renderParams(params) + '
'; + } + } + }, + {data: 'priority', width: "2em"}, + {data: 'displayTime'}, + { + className: 'details-control', + orderable: false, + data: null, + render: function (data, type, row) { + return Mustache.render(templates.actionsTemplate, row); + } + } + ] + }); + + dt.on('draw', updateCurrentFilter); + + $('#serverSide').html('
'); + + // If using server-side filter we need to updateTasks every time the filter changes + + $('#taskTable_filter').on('keyup paste', 'input', function () { + if ($('#serverSideCheckbox')[0].checked) { + clearTimeout(typingTimer); + if ($(this).val) { + typingTimer = setTimeout(updateTasks, 400); + } + } + }); + + processHashChange(); + updateTasks(); + bindListEvents(); + + $('#taskTable tbody').on('click', 'td.details-control .showError', function () { + var tr = $(this).closest('tr'); + var row = dt.row( tr ); + var data = row.data(); + luigi.getErrorTrace(data.taskId, function(error) { + showErrorTrace(error); + }); + } ); + + $('#errorModal').on('click', '#expandModal', function () { + minMaxWindow(this); + }); + + $('#taskTable tbody').on('click', 'td.details-control .forgiveFailures', function (ev) { + var that = $(this); + var tr = that.closest('tr'); + var row = dt.row( tr ); + var data = row.data(); + luigi.forgiveFailures(data.taskId, function(data) { + if (ev.altKey) { + updateTasks(); // update may not be cheap + } else { + that.tooltip('hide'); + that.remove(); + } + }); + } ); + + $('#taskTable tbody').on('click', 'td.details-control .markAsDone', function (ev) { + var that = $(this); + var tr = that.closest('tr'); + var row = dt.row( tr ); + var data = row.data(); + luigi.markAsDone(data.taskId, function(data) { + if (ev.altKey) { + updateTasks(); // update may not be cheap + } else { + that.tooltip('hide'); + that.remove(); + } + }); + } ); + + $('#taskTable tbody').on('click', 'td.details-control .re-enable-button', function (ev) { + var that = $(this); + luigi.reEnable(that.attr("data-task-id"), function(data) { + if (ev.altKey) { + updateTasks(); // update may not be cheap + } else { + that.tooltip('hide'); + that.remove(); + } + }); + }); + + $('#taskTable tbody').on('click', 'td.details-control .statusMessage', function () { + var data = $(this).data(); + showStatusMessage(data); + }); + + $('#taskTable tbody').on('click', 'td.details-control .schedulerMessage', function () { + var data = $(this).data(); + showSchedulerMessageModal(data); + }); + + $('.navbar-nav').on('click', 'a', function () { + var tabName = $(this).data('tab'); + updateSidebar(tabName); + }); + + $('#workerList').on('show.bs.modal', '#disableWorkerModal', function (event) { + var triggerButton = $(event.relatedTarget); + $('#disableWorkerButton').data('trigger', triggerButton); + }); + + $('#workerList').on('click', '#disableWorkerButton', function() { + var triggerButton = $(this).data('trigger'); + var worker = triggerButton.data('worker'); + + luigi.disableWorker(worker); + + // show the worker as disabled in the visualiser + var box = triggerButton.parents('.box').addClass('box-solid box-default'); + + // remove the worker tools + box.find('.box-tools').remove(); + }); + + $('#workerList').on('click', '#btn-increment-workers', function($event) { + var worker = $(this).data("worker"); + var $label = $('#workerList').find('#label-n-workers[data-worker="' + worker + '"]'); + var n = parseInt($label.text()); + if (!isNaN(n)) { + updateWorkerProcesses(worker, n + 1); + } + $event.preventDefault(); + }); + + $('#workerList').on('click', '#btn-decrement-workers', function($event) { + var worker = $(this).data("worker"); + var $label = $('#workerList').find('#label-n-workers[data-worker="' + worker + '"]'); + var n = parseInt($label.text()); + if (!isNaN(n)) { + updateWorkerProcesses(worker, n - 1); + } + $event.preventDefault(); + }); + + $('#workerList').on('show.bs.modal', '#setWorkersModal', function($event) { + $('#setWorkersButton').data('worker', $($event.relatedTarget).data('worker')); + var $input = $(this).find('#setWorkersInput').on('keypress', function($event) { + if (event.keyCode == 13) { + $('#workerList').find('#setWorkersButton').trigger('click'); + } + $event.stopPropagation(); + }); + setTimeout(function() { + $input.focus(); + }.bind(this), 600); + }); + + $('#workerList').on('hidden.bs.modal', '#setWorkersModal', function() { + $(this).find('#setWorkersInput').off('keypress').val(''); + }); + + $('#workerList').on('click', '#setWorkersButton', function($event) { + var worker = $(this).data('worker'); + var n = parseInt($("#setWorkersInput").val()); + if (!isNaN(n)) { + updateWorkerProcesses(worker, n); + } + $event.preventDefault(); + }); + + $('#resourceList').on('click', '.btn-increment-resources', function($event) { + $event.preventDefault(); + var resource = $(this).data('resource'); + var count = currentResourceCount(resource); + updateResourceCount(resource, count + 1); + }); + + $('#resourceList').on('click', '.btn-decrement-resources', function($event) { + $event.preventDefault(); + var resource = $(this).data('resource'); + var count = currentResourceCount(resource); + updateResourceCount(resource, count - 1); + }); + + $('#resourceList').on('show.bs.modal', '#setResourcesModal', function($event) { + $('#setResourcesButton').data('resource', $($event.relatedTarget).data('resource')); + var $input = $(this).find('#setResourcesInput').on('keypress', function($event) { + if (event.keyCode == 13) { + $('#resourceList').find('#setResourcesButton').trigger('click'); + } + $event.stopPropagation(); + }); + setTimeout(function() { + $input.focus(); + }.bind(this), 600); + }); + + $('#resourceList').on('hidden.bs.modal', '#setResourcesModal', function() { + $(this).find('#setResourcesInput').off('keypress').val(''); + }); + + $('#resourceList').on('click', '#setResourcesButton', function($event) { + var resource = $(this).data('resource'); + var n = parseInt($("#setResourcesInput").val()); + updateResourceCount(resource, n); + $event.preventDefault(); + }); + $('.js-nav-link').click(function(e) { + // User followed tab from navigation link. Copy state from fields to hash. + e.preventDefault(); + var state = {}; + var tabId = $(this).attr('data-tab'); + + if (tabId == 'taskList') { + var order = dt.order(); + var search = dt.search(); + state.tab = 'tasks'; + + if ($('select[name=taskTable_length]').val() !== '10') { + // Add length to hash only if the value is not default. + state.length = $('select[name=taskTable_length]').val(); + } + + if ($('#serverSideCheckbox').is(':checked')) { + state.filterOnServer = '1'; + } + + var family = $('#familySidebar li.active').attr('data-task'); + if (family) { + state.family = family; + } else { + delete state.family; + } + + if (currentFilter.taskCategory.length > 0) { + state.statuses = JSON.stringify(currentFilter.taskCategory); + } else { + delete state.statuses; + } + + if (search) { + state.search__search = search; + } + + if (order.length > 0) { + state.order = '' + order[0][0] + ',' + order[0][1]; + } + + } else if (tabId == 'dependencyGraph') { + state.tab = 'graph'; + + // Get state from fields. + + if ($('#hideDoneCheckbox').is(':checked')) { + state.hideDone = '1'; + } + + if ($('#idTaskForm input.search-query').val()) { + state.taskId = $('#idTaskForm input.search-query').val(); + } + + if ($('#invertCheckbox').is(':checked')) { + state.invert = '1'; + } + + state.visType = $('input[name=vis-type]:checked').val(); + } else if (tabId == 'workerList') { + state.tab = 'workers'; + } else if (tabId == 'resourceList') { + state.resources = JSON.stringify(expandedResources()); + state.tab = 'resources'; + } + + location.hash = '#' + URI.buildQuery(state); + }); + + processHashChange(); + }); +}