| /* |
| * 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. |
| */ |
| /** |
| * Displays details on an activity/task |
| */ |
| define([ |
| "underscore", "jquery", "backbone", "brooklyn-utils", "view/viewutils", "moment", |
| "model/task-summary", |
| "text!tpl/apps/activity-details.html", "text!tpl/apps/activity-table.html", |
| |
| "bootstrap", "jquery-datatables", "datatables-extensions" |
| ], function (_, $, Backbone, Util, ViewUtils, moment, |
| TaskSummary, |
| ActivityDetailsHtml, ActivityTableHtml) { |
| |
| var activityTableTemplate = _.template(ActivityTableHtml), |
| activityDetailsTemplate = _.template(ActivityDetailsHtml); |
| |
| function makeActivityTable($el) { |
| $el.html(_.template(ActivityTableHtml)); |
| var $subTable = $('.activity-table', $el); |
| $subTable.attr('width', 569-6-6 /* subtract padding */) |
| |
| return ViewUtils.myDataTable($subTable, { |
| "fnRowCallback": function( nRow, aData, iDisplayIndex, iDisplayIndexFull ) { |
| $(nRow).attr('id', aData[0]) |
| $(nRow).addClass('activity-row') |
| }, |
| "aoColumnDefs": [ { |
| "mRender": function ( data, type, row ) { return Util.escape(data) }, |
| "aTargets": [ 1, 2, 3 ] |
| }, { |
| "mRender": function ( data, type, row ) { return Util.escape(data) }, |
| "bVisible": false, |
| "aTargets": [ 0 ] |
| } ], |
| "aaSorting":[] // default not sorted (server-side order) |
| }); |
| } |
| |
| var ActivityDetailsView = Backbone.View.extend({ |
| template: activityDetailsTemplate, |
| taskLink: '', |
| task: null, |
| /* children of this task; see HasTaskChildren for difference between this and sub(mitted)Tasks */ |
| childrenTable: null, |
| /* tasks in the current execution context (this.collections) whose submittedByTask |
| * is the task we are drilled down on. this defaults to the passed in collection, |
| * which will be the last-viewed entity's exec-context; when children cross exec-context |
| * boundaries we have to rewire to point to the current entity's exec-context / tasks */ |
| subtasksTable: null, |
| children: null, |
| breadcrumbs: [], |
| firstLoad: true, |
| events:{ |
| "click #activities-children-table .activity-table tr":"childrenRowClick", |
| "click #activities-submitted-table .activity-table tr":"submittedRowClick", |
| 'click .showDrillDownSubmittedByAnchor':'showDrillDownSubmittedByAnchor', |
| 'click .showDrillDownBlockerOfAnchor':'showDrillDownBlockerOfAnchor', |
| 'click .backDrillDown':'backDrillDown' |
| }, |
| // requires taskLink or task; breadcrumbs is optional |
| initialize:function () { |
| var that = this; |
| this.taskLink = this.options.taskLink; |
| this.taskId = this.options.taskId; |
| if (this.options.task) |
| this.task = this.options.task; |
| else if (this.options.tabView) |
| this.task = this.options.tabView.collection.get(this.taskId); |
| if (!this.taskLink && this.task) this.taskLink = this.task.get('links').self; |
| if (!this.taskLink && this.taskId) this.taskLink = "v1/activities/"+encodeURIComponent(this.taskId); |
| |
| this.tabView = this.options.tabView || null; |
| |
| if (this.options.breadcrumbs) this.breadcrumbs = this.options.breadcrumbs; |
| |
| this.$el.html(this.template({ taskLink: this.taskLink, taskId: this.taskId, task: this.task, breadcrumbs: this.breadcrumbs })); |
| this.$el.addClass('activity-detail-panel'); |
| |
| this.childrenTable = makeActivityTable(this.$('#activities-children-table')); |
| this.subtasksTable = makeActivityTable(this.$('#activities-submitted-table')); |
| |
| ViewUtils.attachToggler(this.$el) |
| |
| if (this.task) { |
| this.renderTask() |
| this.setUpPolling() |
| } else { |
| ViewUtils.fadeToIndicateInitialLoad(this.$el); |
| this.$el.css('cursor', 'wait') |
| $.get(this.taskLink, function(data) { |
| ViewUtils.cancelFadeOnceLoaded(that.$el); |
| that.task = new TaskSummary.Model(data) |
| that.renderTask() |
| that.setUpPolling(); |
| }).fail(function() { log("unable to load "+that.taskLink) }) |
| } |
| |
| // initial subtasks may be available from parent, so try to render those |
| // (reliable polling for subtasks, and for children, is set up in setUpPolling ) |
| this.renderSubtasks() |
| }, |
| |
| refreshNow: function(initial) { |
| var that = this |
| $.get(this.taskLink, function(data) { |
| that.task = new TaskSummary.Model(data) |
| that.renderTask() |
| if (initial) that.setUpPolling(); |
| }) |
| }, |
| renderTask: function() { |
| // update task fields |
| var that = this, firstLoad = this.firstLoad; |
| this.firstLoad = false; |
| |
| if (firstLoad && this.task) { |
| // log("rendering "+firstLoad+" "+this.task.get('isError')+" "+this.task.id); |
| if (this.task.get('isError')) { |
| // on first load, expand the details if there is a problem |
| var $details = this.$(".toggler-region.task-detail .toggler-header"); |
| ViewUtils.showTogglerClickElement($details); |
| } |
| } |
| |
| this.updateFields('displayName', 'entityDisplayName', 'id', 'description', 'currentStatus', 'blockingDetails'); |
| this.updateFieldWith('blockingTask', |
| function(v) { |
| return "<a class='showDrillDownBlockerOfAnchor handy' link='"+encodeURI(v.link)+"' id='"+_.escape(v.metadata.id)+"'>"+ |
| that.displayTextForLinkedTask(v)+"</a>" }) |
| this.updateFieldWith('result', |
| function(v) { |
| // use display string (JSON.stringify(_.escape(v)) because otherwise list of [null,null] is just "," |
| var vs = Util.toDisplayString(v); |
| if (vs.trim().length==0) { |
| return " (empty result)"; |
| } else if (vs.length<20 && !/\r|\n/.exec(v)) { |
| return " with result: <span class='result-literal'>"+vs+"</span>"; |
| } else { |
| return "<div class='result-literal'>"+vs.replace(/\n+/g,"<br>")+"</div>" |
| } |
| }) |
| this.updateFieldWith('tags', function(tags) { |
| var tagBody = ""; |
| for (var tag in tags) { |
| tagBody += "<div class='activity-tag-giftlabel'>"+Util.toDisplayString(tags[tag])+"</div>"; |
| } |
| return tagBody; |
| }) |
| |
| var submitTimeUtc = this.updateFieldWith('submitTimeUtc', |
| function(v) { return v <= 0 ? "-" : moment(v).format('D MMM YYYY H:mm:ss.SSS')+" <i>"+moment(v).fromNow()+"</i>" }) |
| var startTimeUtc = this.updateFieldWith('startTimeUtc', |
| function(v) { return v <= 0 ? "-" : moment(v).format('D MMM YYYY H:mm:ss.SSS')+" <i>"+moment(v).fromNow()+"</i>" }) |
| this.updateFieldWith('endTimeUtc', |
| function(v) { return v <= 0 ? "-" : moment(v).format('D MMM YYYY H:mm:ss.SSS')+" <i>"+moment(v).from(startTimeUtc, true)+" later</i>" }) |
| |
| ViewUtils.updateTextareaWithData(this.$(".task-json .for-textarea"), |
| Util.toTextAreaString(this.task), false, false, 150, 400) |
| |
| ViewUtils.updateTextareaWithData(this.$(".task-detail .for-textarea"), |
| this.task.get('detailedStatus'), false, false, 30, 250) |
| |
| this.updateFieldWith('streams', |
| function(streams) { |
| // Stream names presented alphabetically |
| var keys = _.keys(streams); |
| keys.sort(); |
| var result = ""; |
| for (var i = 0; i < keys.length; i++) { |
| var name = keys[i]; |
| var stream = streams[name]; |
| result += "<div class='activity-stream-div'>" + |
| "<span class='activity-label'>" + |
| _.escape(name) + |
| "</span><span>" + |
| "<a href='" + encodeURI(stream.link) + "'>download</a>" + |
| (stream.metadata["sizeText"] ? " (" + _.escape(stream.metadata["sizeText"]) + ")" : "") + |
| "</span></div>"; |
| } |
| return result; |
| }); |
| |
| this.updateFieldWith('submittedByTask', |
| function(v) { return "<a class='showDrillDownSubmittedByAnchor handy' link='"+encodeURI(v.link)+"' id='"+_.escape(v.metadata.id)+"'>"+ |
| that.displayTextForLinkedTask(v)+"</a>" }) |
| |
| if (this.task.get("children").length==0) |
| this.$('.toggler-region.tasks-children').hide(); |
| }, |
| setUpPolling: function() { |
| var that = this |
| |
| // on first load, clear any funny cursor |
| this.$el.css('cursor', 'auto') |
| |
| this.task.url = this.taskLink; |
| this.task.on("all", this.renderTask, this) |
| |
| ViewUtils.get(this, this.taskLink, function(data) { |
| // if we can get the data, then start fetching certain things repeatedly |
| // (would be good to skip the immediate "doitnow" below but not a big deal) |
| ViewUtils.fetchRepeatedlyWithDelay(that, that.task, { doitnow: true }); |
| |
| // and set up to load children (now that the task is guaranteed to be loaded) |
| that.children = new TaskSummary.Collection() |
| that.children.url = that.task.get("links").children |
| that.children.on("reset", that.renderChildren, that) |
| ViewUtils.fetchRepeatedlyWithDelay(that, that.children, { |
| fetchOptions: { reset: true }, doitnow: true, fadeTarget: that.$('.tasks-children') }); |
| }).fail( function() { that.$('.toggler-region.tasks-children').hide() } ); |
| |
| |
| $.get(this.task.get("links").entity, function(entity) { |
| if (that.collection==null || entity.links.activities != that.collection.url) { |
| // need to rewire collection to point to the right ExecutionContext |
| that.collection = new TaskSummary.Collection() |
| that.collection.url = entity.links.activities |
| that.collection.on("reset", that.renderSubtasks, that) |
| ViewUtils.fetchRepeatedlyWithDelay(that, that.collection, { |
| fetchOptions: { reset: true }, doitnow: true, fadeTarget: that.$('.tasks-submitted') }); |
| } else { |
| that.collection.on("reset", that.renderSubtasks, that) |
| that.collection.fetch({reset: true}); |
| } |
| }); |
| }, |
| |
| renderChildren: function() { |
| var that = this |
| var children = this.children |
| ViewUtils.updateMyDataTable(this.childrenTable, children, function(task, index) { |
| return [ task.get("id"), |
| (task.get("entityId") && task.get("entityId")!=that.task.get("entityId") ? task.get("entityDisplayName") + ": " : "") + |
| task.get("displayName"), |
| task.get("submitTimeUtc") <= 0 ? "-" : moment(task.get("submitTimeUtc")).calendar(), |
| task.get("currentStatus") |
| ]; |
| }); |
| if (children && children.length>0) { |
| this.$('.toggler-region.tasks-children').show(); |
| } else { |
| this.$('.toggler-region.tasks-children').hide(); |
| } |
| }, |
| renderSubtasks: function() { |
| var that = this |
| var taskId = this.taskId || (this.task ? this.task.id : null); |
| if (!this.collection) { |
| this.$('.toggler-region.tasks-submitted').hide(); |
| return; |
| } |
| if (!taskId) { |
| // task not available yet; just wait for it to be loaded |
| // (and in worst case, if it can't be loaded, this panel stays faded) |
| return; |
| } |
| |
| // find tasks submitted by this one which aren't included as children |
| // this uses collections -- which is everything in the current execution context |
| var subtasks = [] |
| for (var taskI in this.collection.models) { |
| var task = this.collection.models[taskI] |
| var submittedBy = task.get("submittedByTask") |
| if (submittedBy!=null && submittedBy.metadata!=null && submittedBy.metadata["id"] == taskId && |
| (!this.children || this.children.get(task.id)==null)) { |
| subtasks.push(task) |
| } |
| } |
| ViewUtils.updateMyDataTable(this.subtasksTable, subtasks, function(task, index) { |
| return [ task.get("id"), |
| (task.get("entityId") && (!that.task || task.get("entityId")!=that.task.get("entityId")) ? task.get("entityDisplayName") + ": " : "") + |
| task.get("displayName"), |
| task.get("submitTimeUtc") <= 0 ? "-" : moment(task.get("submitTimeUtc")).calendar(), |
| task.get("currentStatus") |
| ]; |
| }); |
| if (subtasks && subtasks.length>0) { |
| this.$('.toggler-region.tasks-submitted').show(); |
| } else { |
| this.$('.toggler-region.tasks-submitted').hide(); |
| } |
| }, |
| |
| displayTextForLinkedTask: function(v) { |
| return v.metadata.taskName ? |
| (v.metadata.entityDisplayName ? _.escape(v.metadata.entityDisplayName)+" <b>"+_.escape(v.metadata.taskName)+"</b>" : |
| _.escape(v.metadata.taskName)) : |
| v.metadata.taskId ? _.escape(v.metadata.taskId) : |
| _.escape(v.link) |
| }, |
| updateField: function(field) { |
| return this.updateFieldWith(field, _.escape) |
| }, |
| updateFields: function() { |
| _.map(arguments, this.updateField, this); |
| }, |
| updateFieldWith: function(field, f) { |
| var v = this.task.get(field) |
| if (v !== undefined && v != null && |
| (typeof v !== "object" || _.size(v) > 0)) { |
| this.$('.updateField-'+_.escape(field), this.$el).html( f(v) ); |
| this.$('.ifField-'+_.escape(field), this.$el).show(); |
| } else { |
| // blank if there is no value |
| this.$('.updateField-'+_.escape(field)).empty(); |
| this.$('.ifField-'+_.escape(field)).hide(); |
| } |
| return v |
| }, |
| childrenRowClick:function(evt) { |
| var row = $(evt.currentTarget).closest("tr"); |
| var id = row.attr("id"); |
| this.showDrillDownTask("subtask of", this.children.get(id).get("links").self, id, this.children.get(id)) |
| }, |
| submittedRowClick:function(evt) { |
| var row = $(evt.currentTarget).closest("tr"); |
| var id = row.attr("id"); |
| // submitted tasks are guaranteed to be in the collection, so this is safe |
| this.showDrillDownTask("subtask of", this.collection.get(id).get('links').self, id) |
| }, |
| |
| showDrillDownSubmittedByAnchor: function(from) { |
| var $a = $(from.target).closest('a'); |
| this.showDrillDownTask("submitter of", $a.attr("link"), $a.attr("id")) |
| }, |
| showDrillDownBlockerOfAnchor: function(from) { |
| var $a = $(from.target).closest('a'); |
| this.showDrillDownTask("blocker of", $a.attr("link"), $a.attr("id")) |
| }, |
| showDrillDownTask: function(relation, newTaskLink, newTaskId, newTask) { |
| var that = this; |
| |
| var newBreadcrumbs = [ relation + ' ' + |
| this.task.get('entityDisplayName') + ' ' + |
| this.task.get('displayName') ].concat(this.breadcrumbs) |
| |
| var activityDetailsPanel = new ActivityDetailsView({ |
| taskLink: newTaskLink, |
| taskId: newTaskId, |
| task: newTask, |
| tabView: that.tabView, |
| collection: this.collection, |
| breadcrumbs: newBreadcrumbs |
| }); |
| activityDetailsPanel.addToView(this.$el); |
| }, |
| addToView: function(parent) { |
| if (this.parent) { |
| log("WARN: adding details to view when already added") |
| this.parent = parent; |
| } |
| |
| if (Backbone.history && (!this.tabView || !this.tabView.openingQueuedTasks)) { |
| Backbone.history.navigate(Backbone.history.fragment+"/"+"subtask"+"/"+encodeURIComponent(this.taskId)); |
| } |
| |
| var $t = parent.closest('.slide-panel'); |
| var $t2 = $t.after('<div>').next(); |
| $t2.addClass('slide-panel'); |
| |
| // load the drill-down page |
| $t2.html(this.render().el) |
| |
| var speed = (!this.tabView || !this.tabView.openingQueuedTasks) ? 300 : 0; |
| $t.animate({ |
| left: -600 |
| }, speed, function() { |
| $t.hide() |
| }); |
| |
| $t2.show().css({ |
| left: 600 |
| , top: 0 |
| }).animate({ |
| left: 0 |
| }, speed); |
| }, |
| backDrillDown: function(event) { |
| // log("activities drill back from "+this.taskLink) |
| var that = this |
| var $t2 = this.$el.closest('.slide-panel') |
| var $t = $t2.prev() |
| |
| if (Backbone.history) { |
| var fragment = Backbone.history.fragment |
| var thisLoc = fragment.indexOf("/subtask/"+encodeURIComponent(this.taskId)); |
| if (thisLoc>=0) |
| Backbone.history.navigate( fragment.substring(0, thisLoc) ); |
| } |
| |
| $t2.animate({ |
| left: 569 //prevTable.width() |
| }, 300, function() { |
| that.$el.empty() |
| $t2.remove() |
| that.remove() |
| }); |
| |
| $t.show().css({ |
| left: -600 //-($t2.width()) |
| }).animate({ |
| left: 0 |
| }, 300); |
| } |
| }); |
| |
| return ActivityDetailsView; |
| }); |