2 new commits in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/921aa1c775b5/ Changeset: 921aa1c775b5 User: dannon Date: 2014-10-16 17:41:10+00:00 Summary: Slightly better handling of s3 object store conf, allowing connection element to be optional Affected #: 1 file diff -r 9f5491972e72ee38e011d49e891303e47bca29d9 -r 921aa1c775b54911c149fe3c8a1dc9645945a83d lib/galaxy/objectstore/s3.py --- a/lib/galaxy/objectstore/s3.py +++ b/lib/galaxy/objectstore/s3.py @@ -77,7 +77,11 @@ b_xml = config_xml.findall('bucket')[0] self.bucket = b_xml.get('name') self.use_rr = b_xml.get('use_reduced_redundancy', False) - cn_xml = config_xml.findall('connection')[0] + cn_xml = config_xml.findall('connection') + if not cn_xml: + cn_xml = {} + else: + cn_xml = cn_xml[0] self.host = cn_xml.get('host', None) self.port = int(cn_xml.get('port', 6000)) self.is_secure = cn_xml.get('is_secure', True) https://bitbucket.org/galaxy/galaxy-central/commits/b1d9b4f49abb/ Changeset: b1d9b4f49abb User: dannon Date: 2014-10-16 17:41:22+00:00 Summary: Merge. Affected #: 65 files diff -r 921aa1c775b54911c149fe3c8a1dc9645945a83d -r b1d9b4f49abb5f27bf54e01901fc37fe2fe20681 client/galaxy/scripts/mvc/collection/collection-li.js --- a/client/galaxy/scripts/mvc/collection/collection-li.js +++ b/client/galaxy/scripts/mvc/collection/collection-li.js @@ -100,8 +100,6 @@ /** add the DCE class to the list item */ className : ListItemView.prototype.className + " dataset-collection-element", - /** jq fx speed for this view */ - fxSpeed : 'fast', /** set up */ initialize : function( attributes ){ diff -r 921aa1c775b54911c149fe3c8a1dc9645945a83d -r b1d9b4f49abb5f27bf54e01901fc37fe2fe20681 client/galaxy/scripts/mvc/history/history-structure-view.js --- /dev/null +++ b/client/galaxy/scripts/mvc/history/history-structure-view.js @@ -0,0 +1,421 @@ +define([ + 'mvc/job/job-model', + 'mvc/job/job-li', + 'mvc/history/history-content-model', + 'mvc/history/job-dag', + 'mvc/base-mvc', + 'utils/localization', + 'libs/d3' +], function( JOB, JOB_LI, HISTORY_CONTENT, JobDAG, BASE_MVC, _l ){ +// ============================================================================ +/* TODO: +change component = this to something else + +*/ +// ============================================================================ +/** + * + */ +var HistoryStructureComponent = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend({ + + //logger : console, + + className : 'history-structure-component', + + initialize : function( attributes ){ + this.log( this + '(HistoryStructureComponent).initialize:', attributes ); + this.component = attributes.component; + + this._jobLiMap = {}; + this._createJobModels(); + + this.layout = this._createLayout( attributes.layoutOptions ); + }, + + _createJobModels : function(){ + var view = this; + view.component.eachVertex( function( vertex ){ + var jobJSON = vertex.data.job, + job = new JOB.Job( jobJSON ); + + // get the models of the outputs for this job from the history + var outputModels = _.map( job.get( 'outputs' ), function( output ){ + //note: output is { src: 'hda/dataset_collection', id: <some id> } + // job output doesn't *quite* match up to normal typeId + var type = output.src === 'hda'? 'dataset' : 'dataset_collection', + typeId = HISTORY_CONTENT.typeIdStr( type, output.id ); + return view.model.contents.get( typeId ); + }); + // set the collection (HistoryContents) for the job to that json (setting historyId for proper ajax urls) + job.outputCollection.reset( outputModels ); + job.outputCollection.historyId = view.model.id; + //this.debug( job.outputCollection ); + + // create the bbone view for the job (to be positioned later accrd. to the layout) and cache + var li = new JOB_LI.JobListItemView({ model: job }); + //var li = new JOB_LI.JobListItemView({ model: job }); + li.render( 0 ).$el.appendTo( view.$el ); + view._jobLiMap[ job.id ] = li; + view._setUpJobListeners( li ); + + }); + return view.jobs; + }, + + _setUpJobListeners : function( jobLi ){ + // update the layout during expansion and collapsing of job and output li's + jobLi.on( 'expanding expanded collapsing collapsed', this.render, this ); + jobLi.foldout.on( 'view:expanding view:expanded view:collapsing view:collapsed', this.render, this ); + }, + + layoutDefaults : { + paddingTop : 8, + paddingLeft : 20, + linkSpacing : 16, + jobHeight : 308, + jobWidthSpacing : 320, + linkAdjX : 4, + linkAdjY : 0 + }, + + _createLayout : function( options ){ + options = _.defaults( _.clone( options || {} ), this.layoutDefaults ); + var view = this, + vertices = _.values( view.component.vertices ), + layout = _.extend( options, { + nodeMap : {}, + links : [], + el : { width: 0, height: 0 }, + svg : { width: 0, height: 0, top: 0, left: 0 } + }); + + vertices.forEach( function( v, j ){ + var node = { name: v.name, x: 0, y: 0 }; + layout.nodeMap[ v.name ] = node; + }); + + view.component.edges( function( e ){ + var link = { + source: e.source, + target: e.target + }; + layout.links.push( link ); + }); + //this.debug( JSON.stringify( layout, null, ' ' ) ); + return layout; + }, + + _updateLayout : function(){ + var view = this, + layout = view.layout; + + layout.svg.height = layout.paddingTop + ( layout.linkSpacing * _.size( layout.nodeMap ) ); + layout.el.height = layout.svg.height + layout.jobHeight; + +//TODO:?? can't we just alter the component v and e's directly? + // layout the job views putting jobWidthSpacing btwn each + var x = layout.paddingLeft, + y = layout.svg.height; + _.each( layout.nodeMap, function( node, jobId ){ + //this.debug( node, jobId ); + node.x = x; + node.y = y; + x += layout.jobWidthSpacing; + }); + layout.el.width = layout.svg.width = Math.max( layout.el.width, x ); + + // layout the links - connecting each job by it's main coords (currently) +//TODO: somehow adjust the svg height based on the largest distance the longest connection needs + layout.links.forEach( function( link ){ + var source = layout.nodeMap[ link.source ], + target = layout.nodeMap[ link.target ]; + link.x1 = source.x + layout.linkAdjX; + link.y1 = source.y + layout.linkAdjY; + link.x2 = target.x + layout.linkAdjX; + link.y2 = target.y + layout.linkAdjY; + }); + //this.debug( JSON.stringify( layout, null, ' ' ) ); + return this.layout; + }, + + render : function( options ){ + this.debug( this + '.render:', options ); + var view = this; + + function _render(){ + view._updateLayout(); + // set up the display containers + view.$el + .width( view.layout.el.width ) + .height( view.layout.el.height ); + view.renderSVG(); + + // position the job views accrd. to the layout + view.component.eachVertex( function( v ){ +//TODO:? liMap needed - can't we attach to vertex? + var li = view._jobLiMap[ v.name ], + position = view.layout.nodeMap[ li.model.id ]; + //this.debug( position ); + li.$el.css({ top: position.y, left: position.x }); + }); + } +//TODO: hack - li's invisible in updateLayout without this delay + if( !this.$el.is( ':visible' ) ){ + _.delay( _render, 0 ); + } else { + _render(); + } + return this; + }, + + renderSVG : function(){ + var view = this, + layout = view.layout; + + var svg = d3.select( this.el ).select( 'svg' ); + if( svg.empty() ){ + svg = d3.select( this.el ).append( 'svg' ); + } + + svg + .attr( 'width', layout.svg.width ) + .attr( 'height', layout.svg.height ); + + function highlightConnect( d ){ + d3.select( this ).classed( 'highlighted', true ); + view._jobLiMap[ d.source ].$el.addClass( 'highlighted' ); + view._jobLiMap[ d.target ].$el.addClass( 'highlighted' ); + } + + function unhighlightConnect( d ){ + d3.select( this ).classed( 'highlighted', false ); + view._jobLiMap[ d.source ].$el.removeClass( 'highlighted' ); + view._jobLiMap[ d.target ].$el.removeClass( 'highlighted' ); + } + + var connections = svg.selectAll( '.connection' ) + .data( layout.links ); + + connections + .enter().append( 'path' ) + .attr( 'class', 'connection' ) + .attr( 'id', function( d ){ return d.source + '-' + d.target; }) + .on( 'mouseover', highlightConnect ) + .on( 'mouseout', unhighlightConnect ); + + connections + .attr( 'd', function( d ){ return view._connectionPath( d ); }); + +//TODO: ? can we use tick here to update the links? + + return svg.node(); + }, + + _connectionPath : function( d ){ + var CURVE_X = 0, + controlY = ( ( d.x2 - d.x1 ) / this.layout.svg.width ) * this.layout.svg.height; + return [ + 'M', d.x1, ',', d.y1, ' ', + 'C', + d.x1 + CURVE_X, ',', d.y1 - controlY, ' ', + d.x2 - CURVE_X, ',', d.y2 - controlY, ' ', + d.x2, ',', d.y2 + ].join( '' ); + }, + + events : { + 'mouseover .job.list-item' : function( ev ){ this.highlightConnected( ev.currentTarget, true ); }, + 'mouseout .job.list-item' : function( ev ){ this.highlightConnected( ev.currentTarget, false ); } + }, + + highlightConnected : function( jobElement, highlight ){ + highlight = highlight !== undefined? highlight : true; + + var view = this, + component = view.component, + jobClassFn = highlight? jQuery.prototype.addClass : jQuery.prototype.removeClass, + connectionClass = highlight? 'connection highlighted' : 'connection'; + + //console.debug( 'mouseover', this ); + var $jobs = jobClassFn.call( $( jobElement ), 'highlighted' ), + id = $jobs.attr( 'id' ).replace( 'job-', '' ); + + // immed. ancestors + component.edges({ target: id }).forEach( function( edge ){ + jobClassFn.call( view.$( '#job-' + edge.source ), 'highlighted' ); + view.$( '#' + edge.source + '-' + id ).attr( 'class', connectionClass ); + }); + // descendants + component.vertices[ id ].eachEdge( function( edge ){ + jobClassFn.call( view.$( '#job-' + edge.target ), 'highlighted' ); + view.$( '#' + id + '-' + edge.target ).attr( 'class', connectionClass ); + }); + }, + + toString : function(){ + return 'HistoryStructureComponent(' + this.model.id + ')'; + } +}); + + +// ============================================================================ +/** + * + */ +var VerticalHistoryStructureComponent = HistoryStructureComponent.extend({ + + //logger : console, + + className : HistoryStructureComponent.prototype.className + ' vertical', + + layoutDefaults : { + paddingTop : 8, + paddingLeft : 20, + linkSpacing : 16, + jobWidth : 308, + jobHeight : 308, + initialSpacing : 64, + jobSpacing : 16, + linkAdjX : 0, + linkAdjY : 4 + }, + +//TODO: how can we use the dom height of the job li's - they're not visible when this is called? + _updateLayout : function(){ + var view = this, + layout = view.layout; + //this.info( this.cid, '_updateLayout' ) + + layout.svg.width = layout.paddingLeft + ( layout.linkSpacing * _.size( layout.nodeMap ) ); + layout.el.width = layout.svg.width + layout.jobWidth; + + // reset height - we'll get the max Y below to assign to it + layout.el.height = 0; + +//TODO:?? can't we just alter the component v and e's directly? + var x = layout.svg.width, + y = layout.paddingTop; + _.each( layout.nodeMap, function( node, jobId ){ + //this.debug( node, jobId ); + node.x = x; + node.y = y; + var li = view._jobLiMap[ jobId ]; + if( li.$el.is( ':visible' ) ){ + y += li.$el.height() + layout.jobSpacing; + } else { + y += layout.initialSpacing + layout.jobSpacing; + } + }); + layout.el.height = layout.svg.height = Math.max( layout.el.height, y ); + + // layout the links - connecting each job by it's main coords (currently) + layout.links.forEach( function( link ){ + var source = layout.nodeMap[ link.source ], + target = layout.nodeMap[ link.target ]; + link.x1 = source.x + layout.linkAdjX; + link.y1 = source.y + layout.linkAdjY; + link.x2 = target.x + layout.linkAdjX; + link.y2 = target.y + layout.linkAdjY; + view.debug( 'link:', link.x1, link.y1, link.x2, link.y2, link ); + }); + //this.debug( JSON.stringify( layout, null, ' ' ) ); + view.debug( 'el:', layout.el ); + view.debug( 'svg:', layout.svg ); + return layout; + }, + + _connectionPath : function( d ){ + var CURVE_Y = 0, + controlX = ( ( d.y2 - d.y1 ) / this.layout.svg.height ) * this.layout.svg.width; + return [ + 'M', d.x1, ',', d.y1, ' ', + 'C', + d.x1 - controlX, ',', d.y1 + CURVE_Y, ' ', + d.x2 - controlX, ',', d.y2 - CURVE_Y, ' ', + d.x2, ',', d.y2 + ].join( '' ); + }, + + toString : function(){ + return 'VerticalHistoryStructureComponent(' + this.model.id + ')'; + } +}); + + +// ============================================================================ +/** + * + */ +var HistoryStructureView = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend({ + + //logger : console, + + className : 'history-structure', + + initialize : function( attributes ){ + this.log( this + '(HistoryStructureView).initialize:', attributes, this.model ); +//TODO: to model + this.jobs = attributes.jobs; + this._createDAG(); + }, + + _createDAG : function(){ + this.dag = new JobDAG({ + historyContents : this.model.contents.toJSON(), + jobs : this.jobs, + excludeSetMetadata : true, + excludeErroredJobs : true + }); +//window.dag = this.dag; + this.log( this + '.dag:', this.dag ); + + this._createComponents(); + }, + + _createComponents : function(){ + this.log( this + '._createComponents' ); + var structure = this; +//window.structure = structure; + + structure.componentViews = structure.dag.weakComponentGraphArray().map( function( componentGraph ){ + return structure._createComponent( componentGraph ); + }); + }, + + _createComponent : function( component ){ + this.log( this + '._createComponent:', component ); + //return new HistoryStructureComponent({ + return new VerticalHistoryStructureComponent({ + model : this.model, + component : component + }); + }, + + render : function( options ){ + this.log( this + '.render:', options ); + var structure = this; + + structure.$el.html([ + '<div class="controls"></div>', + '<div class="components"></div>' + ].join( '' )); + + structure.componentViews.forEach( function( component ){ + component.render().$el.appendTo( structure.$components() ); + }); + return structure; + }, + + $components : function(){ + return this.$( '.components' ); + }, + + toString : function(){ + return 'HistoryStructureView(' + ')'; + } +}); + + +// ============================================================================ + return HistoryStructureView; +}); diff -r 921aa1c775b54911c149fe3c8a1dc9645945a83d -r b1d9b4f49abb5f27bf54e01901fc37fe2fe20681 client/galaxy/scripts/mvc/history/job-dag.js --- /dev/null +++ b/client/galaxy/scripts/mvc/history/job-dag.js @@ -0,0 +1,219 @@ +define([ + 'utils/graph', + 'utils/add-logging' +],function( GRAPH, addLogging ){ +// ============================================================================ +var _super = GRAPH.Graph; +/** A Directed acyclic Graph built from a history's job data. + * Reads in job json, filters and process that json, and builds a graph + * using the connections between job inputs and outputs. + */ +var JobDAG = function( options ){ + var self = this; + //this.logger = console; + + // instance vars +//TODO: needed? + self._historyContentsMap = {}; + self.filters = []; + + self._idMap = {}; + self.noInputJobs = []; + self.noOutputJobs = []; + +//TODO: save these? + self.filteredSetMetadata = []; + self.filteredErroredJobs = []; + + _super.call( self, true, null, options ); +}; +JobDAG.prototype = new GRAPH.Graph(); +JobDAG.prototype.constructor = JobDAG; + +// add logging ability - turn off/on using the this.logger statement above +addLogging( JobDAG ); + +// ---------------------------------------------------------------------------- +/** process jobs, options, filters, and any history data, then create the graph */ +JobDAG.prototype.init = function _init( options ){ + options = options || {}; + _super.prototype.init.call( this, options ); + + var self = this, + historyContentsJSON = options.historyContents || []; + jobsJSON = options.jobs || []; + + historyContentsJSON.forEach( function( content, i ){ + self._historyContentsMap[ content.id ] = _.clone( content ); + }); + + self.options = _.defaults( _.omit( options, 'historyContents', 'jobs' ), { + excludeSetMetadata : false + }); + self.filters = self._initFilters(); + +//TODO: O( 3N ) + self.preprocessJobs( _.clone( jobsJSON ) ); + self.createGraph(); + + return self; +}; + +/** add job filters based on options */ +JobDAG.prototype._initFilters = function __initFilters(){ + var self = this, + filters = []; + + if( self.options.excludeSetMetadata ){ + self.filteredSetMetadata = []; + filters.push( function filterSetMetadata( jobData ){ + if( jobData.job.tool_id !== '__SET_METADATA__' ){ return true; } + self.filteredSetMetadata.push( jobData.job.id ); + return false; + }); + } + + if( self.options.excludeErroredJobs ){ + self.filteredErroredJobs = []; + filters.push( function filterErrored( jobData ){ + if( jobData.job.state !== 'error' ){ return true; } + self.filteredErroredJobs.push( jobData.job.id ); + return false; + }); + } + + // all outputs deleted + // all outputs hidden + + if( _.isArray( self.options.filters ) ){ + filters = filters.concat( self.options.filters ); + } + self.debug( 'filters len:', filters.length ); + return filters; +}; + +/** sort the jobs and cache any that pass all filters into _idMap by job.id */ +JobDAG.prototype.preprocessJobs = function _preprocessJobs( jobs ){ + this.info( 'processing jobs' ); + + var self = this; +//TODO:? sorting neccessary? + self.sort( jobs ).forEach( function( job, i ){ + var jobData = self.preprocessJob( job, i ); + if( jobData ){ + self._idMap[ job.id ] = jobData; + } + }); + return self; +}; + +/** sort the jobs based on update time */ +JobDAG.prototype.sort = function _sort( jobs ){ + function cmpCreate( a, b ){ + if( a.update_time > b.update_time ){ return 1; } + if( a.update_time < b.update_time ){ return -1; } + return 0; + } + return jobs.sort( cmpCreate ); +}; + +/** proces input/output, filter based on job data returning data if passing, null if not */ +JobDAG.prototype.preprocessJob = function _preprocessJob( job, index ){ + //this.info( 'preprocessJob', job, index ); + var self = this, + jobData = { index: index, job: job }; + + jobData.inputs = self.datasetMapToIdArray( job.inputs, function( dataset, nameInJob ){ + //TODO:? store output name in self._datasets[ output.id ] from creating job? + }); + if( jobData.inputs.length === 0 ){ + self.noInputJobs.push( job.id ); + } + jobData.outputs = self.datasetMapToIdArray( job.outputs, function( dataset, nameInJob ){ + + }); + if( jobData.outputs.length === 0 ){ + self.noOutputJobs.push( job.id ); + } + + //self.debug( JSON.stringify( jobData, null, ' ' ) ); + // apply filters after processing job allowing access to the additional data above in the filters + for( var i=0; i<self.filters.length; i++ ){ + if( !self.filters[i].call( self, jobData ) ){ + self.debug( 'job', job.id, ' has been filtered out by function:\n', self.filters[i] ); + return null; + } + } + + //self.info( 'preprocessJob returning', jobData ); + return jobData; +}; + +/** make verbose input/output lists more concise, sanity checking along the way + * processFn is called on each input/output and passed the dataset obj (id,src) and the input/output name. + */ +JobDAG.prototype.datasetMapToIdArray = function _datasetMapToIdArray( datasetMap, processFn ){ + var self = this; + return _.map( datasetMap, function( dataset, nameInJob ){ + if( !dataset.id ){ + throw new Error( 'No id on datasetMap: ', JSON.stringify( dataset ) ); + } + if( !dataset.src || dataset.src !== 'hda' ){ + throw new Error( 'Bad src on datasetMap: ', JSON.stringify( dataset ) ); + } + processFn.call( self, dataset, nameInJob ); + return dataset.id; + }); +}; + +/** Walk all the jobs (vertices), attempting to find connections + * between datasets used as both inputs and outputs (edges) + */ +JobDAG.prototype.createGraph = function _createGraph(){ + var self = this; + self.debug( 'connections:' ); + + _.each( self._idMap, function( sourceJobData, sourceJobId ){ + self.debug( '\t', sourceJobId, sourceJobData ); + self.createVertex( sourceJobId, sourceJobData ); + + sourceJobData.usedIn = []; + _.each( sourceJobData.outputs, function( outputDatasetId ){ +//TODO: O(n^2) + _.each( self._idMap, function( targetJobData, targetJobId ){ + if( targetJobData === sourceJobData ){ return; } + + if( targetJobData.inputs.indexOf( outputDatasetId ) !== -1 ){ + self.info( '\t\t\t found connection: ', sourceJobId, targetJobId ); + sourceJobData.usedIn.push({ job: targetJobId, output: outputDatasetId }); + + self.createVertex( targetJobId, targetJobData ); + + self.createEdge( sourceJobId, targetJobId, self.directed, { + dataset : outputDatasetId + }); + } + }); + }); + }); + self.debug( 'job data: ', JSON.stringify( self._idMap, null, ' ' ) ); + return self; +}; + +/** Override to re-sort (ugh) jobs in each component by update time */ +Graph.prototype.weakComponentGraphArray = function(){ + return this.weakComponents().map( function( component ){ +//TODO: this seems to belong above (in sort) - why isn't it preserved? + component.vertices.sort( function cmpCreate( a, b ){ + if( a.data.job.update_time > b.data.job.update_time ){ return 1; } + if( a.data.job.update_time < b.data.job.update_time ){ return -1; } + return 0; + }); + return new Graph( this.directed, component ); + }); +}; + + +// ============================================================================ + return JobDAG; +}); diff -r 921aa1c775b54911c149fe3c8a1dc9645945a83d -r b1d9b4f49abb5f27bf54e01901fc37fe2fe20681 client/galaxy/scripts/mvc/job/job-li.js --- /dev/null +++ b/client/galaxy/scripts/mvc/job/job-li.js @@ -0,0 +1,129 @@ +define([ + 'mvc/list/list-item', + 'mvc/dataset/dataset-list', + "mvc/base-mvc", + "utils/localization" +], function( LIST_ITEM, DATASET_LIST, BASE_MVC, _l ){ +//============================================================================== +var _super = LIST_ITEM.FoldoutListItemView; +/** @class A job view used from within a larger list of jobs. + * Each job itself is a foldout panel of history contents displaying the outputs of this job. + */ +var JobListItemView = _super.extend(/** @lends JobListItemView.prototype */{ + + /** logger used to record this.log messages, commonly set to console */ + //logger : console, + + + className : _super.prototype.className + " job", + id : function(){ + return [ 'job', this.model.get( 'id' ) ].join( '-' ); + }, + + foldoutPanelClass : DATASET_LIST.DatasetList, + + /** Set up: instance vars, options, and event handlers */ + initialize : function( attributes ){ + if( attributes.logger ){ this.logger = this.model.logger = attributes.logger; } + this.log( this + '.initialize:', attributes ); + _super.prototype.initialize.call( this, attributes ); + + /** where should pages from links be displayed? (default to new tab/window) */ + this.linkTarget = attributes.linkTarget || '_blank'; + }, + + /** In this override, add the state as a class for use with state-based CSS */ + _swapNewRender : function( $newRender ){ + _super.prototype._swapNewRender.call( this, $newRender ); + if( this.model.has( 'state' ) ){ + this.$el.addClass( 'state-' + this.model.get( 'state' ) ); + } + return this.$el; + }, + + /** Stub to return proper foldout panel options */ + _getFoldoutPanelOptions : function(){ + var options = _super.prototype._getFoldoutPanelOptions.call( this ); + return _.extend( options, { + collection : this.model.outputCollection, + selecting : false + }); + }, + + // ........................................................................ misc + /** String representation */ + toString : function(){ + return 'JobListItemView(' + this.model + ')'; + } +}); + +// ............................................................................ TEMPLATES +/** underscore templates */ +JobListItemView.prototype.templates = (function(){ +//TODO: move to require text! plugin + + var elTemplate = BASE_MVC.wrapTemplate([ + '<div class="list-element">', + '<div class="id"><%= model.id %></div>', + // errors, messages, etc. + '<div class="warnings"></div>', + + // multi-select checkbox + '<div class="selector">', + '<span class="fa fa-2x fa-square-o"></span>', + '</div>', + // space for title bar buttons - gen. floated to the right + '<div class="primary-actions"></div>', + '<div class="title-bar"></div>', + + // expandable area for more details + '<div class="details"></div>', + '</div>' + ]); + + var titleBarTemplate = BASE_MVC.wrapTemplate([ + // adding a tabindex here allows focusing the title bar and the use of keydown to expand the dataset display + '<div class="title-bar clear" tabindex="0">', + //'<span class="state-icon"></span>', + '<div class="title">', + '<span class="name"><%- job.tool_id %></span>', + '</div>', + '<div class="subtitle"></div>', + '</div>' + ], 'job' ); + + //var subtitleTemplate = BASE_MVC.wrapTemplate([ + // // override this + // '<div class="subtitle">', + // _l( 'Created' ), ': <%= new Date( job.create_time ).toString() %>, ', + // _l( 'Updated' ), ': <%= new Date( job.update_time ).toString() %>', + // '</div>' + //], 'job' ); + // + //var detailsTemplate = BASE_MVC.wrapTemplate([ + // '<div class="details">', + // '<div class="params">', + // '<% _.each( job.params, function( param, paramName ){ %>', + // '<div class="param">', + // '<label class="prompt"><%= paramName %></label>', + // '<span class="value"><%= param %></span>', + // '</div>', + // '<% }) %>', + // '</div>', + // '</div>' + //], 'job' ); + + return _.extend( {}, _super.prototype.templates, { + //el : elTemplate, + titleBar : titleBarTemplate, + //subtitle : subtitleTemplate, + //details : detailsTemplate + }); +}()); + + +//============================================================================= + return { + JobListItemView : JobListItemView + }; +}); diff -r 921aa1c775b54911c149fe3c8a1dc9645945a83d -r b1d9b4f49abb5f27bf54e01901fc37fe2fe20681 client/galaxy/scripts/mvc/job/job-model.js --- /dev/null +++ b/client/galaxy/scripts/mvc/job/job-model.js @@ -0,0 +1,203 @@ +define([ + "mvc/history/history-contents", + "mvc/dataset/states", + "utils/ajax-queue", + "mvc/base-mvc", + "utils/localization" +], function( HISTORY_CONTENTS, STATES, AJAX_QUEUE, BASE_MVC, _l ){ +//============================================================================== +var searchableMixin = BASE_MVC.SearchableModelMixin; +/** @class Represents a job running or ran on the server job handlers. + */ +var Job = Backbone.Model.extend( BASE_MVC.LoggableMixin ).extend( + BASE_MVC.mixin( searchableMixin, /** @lends Job.prototype */{ + + /** logger used to record this.log messages, commonly set to console */ + //logger : console, + + /** default attributes for a model */ + defaults : { + model_class : 'Job', + + tool : null, + exit_code : null, + + inputs : {}, + outputs : {}, + params : {}, + + create_time : null, + update_time : null, + state : STATES.NEW + }, + + /** instance vars and listeners */ + initialize : function( attributes, options ){ + this.debug( this + '(Job).initialize', attributes, options ); + this.outputCollection = attributes.outputCollection || new HISTORY_CONTENTS.HistoryContents([]); + this._setUpListeners(); + }, + + /** set up any event listeners + * event: state:ready fired when this DA moves into/is already in a ready state + */ + _setUpListeners : function(){ + // if the state has changed and the new state is a ready state, fire an event + this.on( 'change:state', function( currModel, newState ){ + this.log( this + ' has changed state:', currModel, newState ); + if( this.inReadyState() ){ + this.trigger( 'state:ready', currModel, newState, this.previous( 'state' ) ); + } + }); + }, + + // ........................................................................ common queries + /** Is this job in a 'ready' state; where 'Ready' states are states where no + * processing is left to do on the server. + */ + inReadyState : function(){ + return _.contains( STATES.READY_STATES, this.get( 'state' ) ); + }, + + /** Does this model already contain detailed data (as opposed to just summary level data)? */ + hasDetails : function(){ + //?? this may not be reliable + return _.isEmpty( this.get( 'outputs' ) ); + }, + + // ........................................................................ ajax + /** root api url */ + urlRoot : (( window.galaxy_config && galaxy_config.root )?( galaxy_config.root ):( '/' )) + 'api/jobs', + //url : function(){ return this.urlRoot; }, + + // ........................................................................ searching + // see base-mvc, SearchableModelMixin + /** what attributes of an Job will be used in a text search */ + searchAttributes : [ + 'tool' + ], + + // ........................................................................ misc + /** String representation */ + toString : function(){ + return [ 'Job(', this.get( 'id' ), ':', this.get( 'tool_id' ), ')' ].join( '' ); + } +})); + + +//============================================================================== +/** @class Backbone collection for Jobs. + */ +var JobCollection = Backbone.Collection.extend( BASE_MVC.LoggableMixin ).extend( +/** @lends JobCollection.prototype */{ + model : Job, + + /** logger used to record this.log messages, commonly set to console */ + //logger : console, + + /** root api url */ + urlRoot : (( window.galaxy_config && galaxy_config.root )?( galaxy_config.root ):( '/' )) + 'api/jobs', + url : function(){ return this.urlRoot; }, + + intialize : function( models, options ){ + console.debug( models, options ); + }, + + // ........................................................................ common queries + /** Get the ids of every item in this collection + * @returns array of encoded ids + */ + ids : function(){ + return this.map( function( item ){ return item.get( 'id' ); }); + }, + + /** Get jobs that are not ready + * @returns array of content models + */ + notReady : function(){ + return this.filter( function( job ){ + return !job.inReadyState(); + }); + }, + + /** return true if any jobs don't have details */ + haveDetails : function(){ + return this.all( function( job ){ return job.hasDetails(); }); + }, + + // ........................................................................ ajax + /** fetches all details for each job in the collection using a queue */ + queueDetailFetching : function(){ + var collection = this, + queue = new AJAX_QUEUE.AjaxQueue( this.map( function( job ){ + return function(){ + return job.fetch({ silent: true }); + }; + })); + queue.done( function(){ + collection.trigger( 'details-loaded' ); + }); + return queue; + }, + + //toDAG : function(){ + // return new JobDAG( this.toJSON() ); + //}, + + // ........................................................................ sorting/filtering + /** return a new collection of jobs whose attributes contain the substring matchesWhat */ + matches : function( matchesWhat ){ + return this.filter( function( job ){ + return job.matches( matchesWhat ); + }); + }, + + // ........................................................................ misc + /** override to get a correct/smarter merge when incoming data is partial */ + set : function( models, options ){ + // arrrrrrrrrrrrrrrrrg... + // (e.g. stupid backbone) + // w/o this partial models from the server will fill in missing data with model defaults + // and overwrite existing data on the client + // see Backbone.Collection.set and _prepareModel + var collection = this; + models = _.map( models, function( model ){ + if( !collection.get( model.id ) ){ return model; } + + // merge the models _BEFORE_ calling the superclass version + var merged = existing.toJSON(); + _.extend( merged, model ); + return merged; + }); + // now call superclass when the data is filled + Backbone.Collection.prototype.set.call( this, models, options ); + }, + + /** String representation. */ + toString : function(){ + return ([ 'JobCollection(', this.length, ')' ].join( '' )); + } + +//----------------------------------------------------------------------------- class vars +}, { + /** class level fn for fetching the job details for all jobs in a history */ + fromHistory : function( historyId ){ + console.debug( this ); + var Collection = this, + collection = new Collection([]); + collection.fetch({ data: { history_id: historyId }}) + .done( function(){ + window.queue = collection.queueDetailFetching(); + + }); + return collection; + } +}); + + +//============================================================================= + return { + Job : Job, + JobCollection : JobCollection + }; +}); diff -r 921aa1c775b54911c149fe3c8a1dc9645945a83d -r b1d9b4f49abb5f27bf54e01901fc37fe2fe20681 client/galaxy/scripts/mvc/list/list-item.js --- a/client/galaxy/scripts/mvc/list/list-item.js +++ b/client/galaxy/scripts/mvc/list/list-item.js @@ -16,8 +16,8 @@ initialize : function( attributes ){ /** are the details of this view expanded/shown or not? */ this.expanded = attributes.expanded || false; - //this.log( '\t expanded:', this.expanded ); - this.fxSpeed = attributes.fxSpeed || this.fxSpeed; + this.log( '\t expanded:', this.expanded ); + this.fxSpeed = attributes.fxSpeed !== undefined? attributes.fxSpeed : this.fxSpeed; }, // ........................................................................ render main @@ -70,7 +70,9 @@ /** empty out the current el, move the $newRender's children in */ _swapNewRender : function( $newRender ){ - return this.$el.empty().attr( 'class', this.className ).append( $newRender.children() ); + return this.$el.empty() + .attr( 'class', _.isFunction( this.className )? this.className(): this.className ) + .append( $newRender.children() ); }, /** set up js behaviors, event handlers for elements within the given container @@ -117,15 +119,8 @@ */ expand : function(){ var view = this; - return view._fetchModelDetails() - .always(function(){ - var $newDetails = view._renderDetails(); - view.$details().replaceWith( $newDetails ); - // needs to be set after the above or the slide will not show - view.expanded = true; - view.$details().slideDown( view.fxSpeed, function(){ - view.trigger( 'expanded', view ); - }); + return view._fetchModelDetails().always( function(){ + view._expand(); }); }, @@ -139,6 +134,24 @@ return jQuery.when(); }, + /** Inner fn called when expand (public) has fetched the details */ + _expand : function(){ + var view = this, + $newDetails = view._renderDetails(); + view.$details().replaceWith( $newDetails ); + // needs to be set after the above or the slide will not show + view.expanded = true; + view.$details().slideDown({ + duration : view.fxSpeed, + step: function(){ + view.trigger( 'expanding', view ); + }, + complete: function(){ + view.trigger( 'expanded', view ); + } + }); + }, + /** Hide the body/details of an HDA. * @fires collapsed when a body has been collapsed */ @@ -146,8 +159,14 @@ this.debug( this + '(ExpandableView).collapse' ); var view = this; view.expanded = false; - this.$details().slideUp( view.fxSpeed, function(){ - view.trigger( 'collapsed', view ); + this.$details().slideUp({ + duration : view.fxSpeed, + step: function(){ + view.trigger( 'collapsing', view ); + }, + complete: function(){ + view.trigger( 'collapsed', view ); + } }); } @@ -204,7 +223,7 @@ $newRender.children( '.warnings' ).replaceWith( this._renderWarnings() ); $newRender.children( '.title-bar' ).replaceWith( this._renderTitleBar() ); $newRender.children( '.primary-actions' ).append( this._renderPrimaryActions() ); - $newRender.find( '.title-bar .subtitle' ).replaceWith( this._renderSubtitle() ); + $newRender.find( '> .title-bar .subtitle' ).replaceWith( this._renderSubtitle() ); return $newRender; }, @@ -375,11 +394,13 @@ * disrespect attributes.expanded if drilldown */ initialize : function( attributes ){ - ListItemView.prototype.initialize.call( this, attributes ); //TODO: hackish if( this.foldoutStyle === 'drilldown' ){ this.expanded = false; } this.foldoutStyle = attributes.foldoutStyle || this.foldoutStyle; this.foldoutPanelClass = attributes.foldoutPanelClass || this.foldoutPanelClass; + + ListItemView.prototype.initialize.call( this, attributes ); + this.foldout = this._createFoldoutPanel(); }, //TODO:?? override to exclude foldout scope? @@ -393,7 +414,7 @@ //TODO: hackish if( this.foldoutStyle === 'drilldown' ){ return $(); } var $newDetails = ListItemView.prototype._renderDetails.call( this ); - return this._attachFoldout( this._createFoldoutPanel(), $newDetails ); + return this._attachFoldout( this.foldout, $newDetails ); }, /** In this override, handle collection expansion. */ @@ -417,7 +438,8 @@ _getFoldoutPanelOptions : function(){ return { // propagate foldout style down - foldoutStyle : this.foldoutStyle + foldoutStyle : this.foldoutStyle, + fxSpeed : this.fxSpeed }; }, @@ -436,25 +458,13 @@ return view._fetchModelDetails() .always(function(){ if( view.foldoutStyle === 'foldout' ){ - view._expandByFoldout(); + view._expand(); } else if( view.foldoutStyle === 'drilldown' ){ view._expandByDrilldown(); } }); }, - /** For foldout, call render details then slide down */ - _expandByFoldout : function(){ - var view = this; - var $newDetails = view._renderDetails(); - view.$details().replaceWith( $newDetails ); - // needs to be set after the above or the slide will not show - view.expanded = true; - view.$details().slideDown( view.fxSpeed, function(){ - view.trigger( 'expanded', view ); - }); - }, - /** For drilldown, set up close handler and fire expanded:drilldown * containing views can listen to this and handle other things * (like hiding themselves) by listening for expanded/collapsed:drilldown @@ -462,7 +472,6 @@ _expandByDrilldown : function(){ var view = this; // attachment and rendering done by listener - view.foldout = this._createFoldoutPanel(); view.foldout.on( 'close', function(){ view.trigger( 'collapsed:drilldown', view, view.foldout ); }); diff -r 921aa1c775b54911c149fe3c8a1dc9645945a83d -r b1d9b4f49abb5f27bf54e01901fc37fe2fe20681 client/galaxy/scripts/mvc/tools/tools-content.js --- a/client/galaxy/scripts/mvc/tools/tools-content.js +++ b/client/galaxy/scripts/mvc/tools/tools-content.js @@ -22,11 +22,16 @@ // get history summary Utils.get({ - url : self.base_url + '?deleted=false', + url : self.base_url + '?deleted=false&state=ok', success : function(response) { // backup summary self.summary = response; + // sort by id + self.summary.sort(function(a, b) { + return a.hid > b.hid ? -1 : (a.hid < b.hid ? 1 : 0); + }); + // log console.debug('tools-content::initialize() - Completed.'); diff -r 921aa1c775b54911c149fe3c8a1dc9645945a83d -r b1d9b4f49abb5f27bf54e01901fc37fe2fe20681 client/galaxy/scripts/mvc/tools/tools-form.js --- a/client/galaxy/scripts/mvc/tools/tools-form.js +++ b/client/galaxy/scripts/mvc/tools/tools-form.js @@ -27,10 +27,8 @@ this.modal = new Ui.Modal.View(); } - // link model/inputs and all options - this.options = options; - this.model = options.model; - this.inputs = options.model.inputs; + // link options + this.options = options; // set element this.setElement('<div/>'); @@ -53,9 +51,13 @@ // reset input element list, which contains the dom elements of each input element (includes also the input field) this.element_list = {}; - // initialize contents + // for now the initial tool model is parsed through the mako + this.model = this.options; + this.inputs = this.options.inputs; + + // request history content and build form this.content = new ToolContent({ - history_id : this.options.history_id, + history_id : self.options.history_id, success : function() { self._buildForm(); } @@ -89,6 +91,51 @@ console.debug('tools-form::refresh() - Recreated data structure. Refresh.'); }, + // build tool model through api call + _buildModel: function() { + // link this + var self = this; + + // construct url + var model_url = galaxy_config.root + 'api/tools/' + this.options.id + '/build?'; + if (this.options.job_id) { + model_url += 'job_id=' + this.options.job_id; + } else { + if (this.options.dataset_id) { + model_url += 'dataset_id=' + this.options.dataset_id; + } else { + var loc = top.location.href; + var pos = loc.indexOf('?'); + if (loc.indexOf('tool_id=') != -1 && pos !== -1) { + model_url += loc.slice(pos + 1); + } + } + } + + // get initial model + Utils.request({ + type : 'GET', + url : model_url, + success : function(response) { + // link model data update options + self.options = $.extend(self.options, response); + self.model = response; + self.inputs = response.inputs; + + // log success + console.debug('tools-form::initialize() - Initial tool model ready.'); + console.debug(response); + + // build form + self._buildForm(); + }, + error : function(response) { + console.debug('tools-form::initialize() - Initial tool model request failed.'); + console.debug(response); + } + }); + }, + // refresh form data _refreshForm: function() { // link this @@ -97,7 +144,7 @@ // finalize data var current_state = this.tree.finalize({ data : function(dict) { - if (dict.values.length > 0 && dict.values[0].src === 'hda') { + if (dict.values.length > 0 && dict.values[0] && dict.values[0].src === 'hda') { return self.content.get({id: dict.values[0].id}).dataset_id; } return null; @@ -111,7 +158,7 @@ // post job Utils.request({ type : 'GET', - url : galaxy_config.root + 'tool_runner/index?tool_id=' + this.options.id + '&form_refresh=True', + url : galaxy_config.root + 'api/tools/' + this.options.id + '/build', data : current_state, success : function(response) { console.debug('tools-form::_refreshForm() - Refreshed inputs/states.'); @@ -132,28 +179,31 @@ // button menu var menu = new Ui.ButtonMenu({ icon : 'fa-gear', - tooltip : 'Click to see a list of available operations.' + tooltip : 'Click to see a list of options.' }); - // add question option - menu.addMenu({ - icon : 'fa-question-circle', - title : 'Question?', - tooltip : 'Ask a question about this tool (Biostar)', - onclick : function() { - window.open(self.options.biostar_url + '/p/new/post/'); - } - }); - - // create search button - menu.addMenu({ - icon : 'fa-search', - title : 'Search', - tooltip : 'Search help for this tool (Biostar)', - onclick : function() { - window.open(self.options.biostar_url + '/t/' + self.options.id + '/'); - } - }); + // configure button selection + if(this.options.biostar_url) { + // add question option + menu.addMenu({ + icon : 'fa-question-circle', + title : 'Question?', + tooltip : 'Ask a question about this tool (Biostar)', + onclick : function() { + window.open(self.options.biostar_url + '/p/new/post/'); + } + }); + + // create search button + menu.addMenu({ + icon : 'fa-search', + title : 'Search', + tooltip : 'Search help for this tool (Biostar)', + onclick : function() { + window.open(self.options.biostar_url + '/t/' + self.options.id + '/'); + } + }); + }; // create share button menu.addMenu({ @@ -185,11 +235,11 @@ }); // switch to classic tool form mako if the form definition is incompatible - //if (this.incompatible) { + if (this.incompatible) { this.$el.hide(); $('#tool-form-classic').show(); return; - //} + } // create portlet this.portlet = new Portlet.View({ @@ -213,12 +263,6 @@ } }); - // configure button selection - if(!this.options.biostar_url) { - button_question.$el.hide(); - button_search.$el.hide(); - } - // append form this.$el.append(this.portlet.$el); diff -r 921aa1c775b54911c149fe3c8a1dc9645945a83d -r b1d9b4f49abb5f27bf54e01901fc37fe2fe20681 client/galaxy/scripts/mvc/tools/tools-jobs.js --- a/client/galaxy/scripts/mvc/tools/tools-jobs.js +++ b/client/galaxy/scripts/mvc/tools/tools-jobs.js @@ -37,7 +37,7 @@ console.debug(job_def); // show progress modal - this.app.modal.show({title: 'Please wait...', body: 'progress', buttons: { 'Close' : function () {self.app.modal.hide();} }}); + this.app.modal.show({title: 'Please wait...', body: 'progress', closing_events: true, buttons: { 'Close' : function () {self.app.modal.hide();} }}); // post job Utils.request({ @@ -55,10 +55,20 @@ var error_messages = self.app.tree.matchResponse(response.message.data); for (var input_id in error_messages) { self._foundError(input_id, error_messages[input_id]); + break; } } else { // show error message with details console.debug(response); + self.app.modal.show({ + title : 'Job submission failed', + body : ToolTemplate.error(job_def), + buttons : { + 'Close' : function() { + self.app.modal.hide(); + } + } + }); } } }); diff -r 921aa1c775b54911c149fe3c8a1dc9645945a83d -r b1d9b4f49abb5f27bf54e01901fc37fe2fe20681 client/galaxy/scripts/mvc/tools/tools-section.js --- a/client/galaxy/scripts/mvc/tools/tools-section.js +++ b/client/galaxy/scripts/mvc/tools/tools-section.js @@ -529,22 +529,12 @@ /** Slider field */ _fieldSlider: function(input_def) { - // set min/max - input_def.min = input_def.min || 0; - input_def.max = input_def.max || 100000; - - // calculate step size - var step = 1; - if (input_def.type == 'float') { - step = (input_def.max - input_def.min) / 10000; - } - // create slider return new Ui.Slider.View({ id : 'field-' + input_def.id, + precise : input_def.type == 'float', min : input_def.min, - max : input_def.max, - step : step + max : input_def.max }); }, diff -r 921aa1c775b54911c149fe3c8a1dc9645945a83d -r b1d9b4f49abb5f27bf54e01901fc37fe2fe20681 client/galaxy/scripts/mvc/tools/tools-select-content.js --- a/client/galaxy/scripts/mvc/tools/tools-select-content.js +++ b/client/galaxy/scripts/mvc/tools/tools-select-content.js @@ -40,7 +40,7 @@ var dataset_options = []; for (var i in datasets) { dataset_options.push({ - label: datasets[i].name, + label: datasets[i].hid + ': ' + datasets[i].name, value: datasets[i].id }); } @@ -67,7 +67,7 @@ var collection_options = []; for (var i in collections) { collection_options.push({ - label: collections[i].name, + label: collections[i].hid + ': ' + collections[i].name, value: collections[i].id }); } @@ -103,20 +103,36 @@ }, /** Return the currently selected dataset values */ - value : function () { - // identify select element - var select = null; - switch(this.current) { - case 'hda': - select = this.select_datasets; - break; - case 'hdca': - select = this.select_collection; - break; + value : function (dict) { + // update current value + if (dict !== undefined) { + try { + // set source + this.current = dict.values[0].src; + this.refresh(); + + // create list + var list = []; + for (var i in dict.values) { + list.push(dict.values[i].id); + } + + // identify select element + switch(this.current) { + case 'hda': + this.select_datasets.value(list); + break; + case 'hdca': + this.select_collection.value(list[0]); + break; + } + } catch (err) { + console.debug('tools-select-content::value() - Skipped.'); + } } // transform into an array - var id_list = select.value(); + var id_list = this._select().value(); if (!(id_list instanceof Array)) { id_list = [id_list]; } @@ -142,12 +158,7 @@ /** Validate current selection */ validate: function() { - switch(this.current) { - case 'hda': - return this.select_datasets.validate(); - case 'hdca': - return this.select_collection.validate(); - } + return this._select().validate(); }, /** Refreshes data selection view */ @@ -162,6 +173,16 @@ this.select_collection.$el.fadeIn(); break; } + }, + + /** Assists in selecting the current field */ + _select: function() { + switch(this.current) { + case 'hdca': + return this.select_collection; + default: + return this.select_datasets; + } } }); diff -r 921aa1c775b54911c149fe3c8a1dc9645945a83d -r b1d9b4f49abb5f27bf54e01901fc37fe2fe20681 client/galaxy/scripts/mvc/tools/tools-template.js --- a/client/galaxy/scripts/mvc/tools/tools-template.js +++ b/client/galaxy/scripts/mvc/tools/tools-template.js @@ -49,7 +49,7 @@ error: function(job_def) { return '<div>' + '<p>' + - 'Sorry, the server could not complete the request. Please contact the Galaxy Team if this error is persistent.' + + 'The server could not complete the request. Please contact the Galaxy Team if this error persists.' + '</p>' + '<textarea class="ui-textarea" disabled style="color: black;" rows="6">' + JSON.stringify(job_def, undefined, 4) + diff -r 921aa1c775b54911c149fe3c8a1dc9645945a83d -r b1d9b4f49abb5f27bf54e01901fc37fe2fe20681 client/galaxy/scripts/mvc/ui/ui-slider.js --- a/client/galaxy/scripts/mvc/ui/ui-slider.js +++ b/client/galaxy/scripts/mvc/ui/ui-slider.js @@ -6,9 +6,11 @@ // options optionsDefault: { value : '', - min : 1, - max : 100, - step : 1 + min : null, + max : null, + step : null, + precise : false, + split : 10000 }, // initialize @@ -18,19 +20,35 @@ // configure options this.options = Utils.merge(options, this.optionsDefault); - + // create new element this.setElement(this._template(this.options)); - // backup slider - this.$slider = this.$el.find('#slider'); + // determine wether to use the slider + this.useslider = this.options.max !== null && this.options.min !== null && this.options.max > this.options.min; + + // set default step size + if (this.options.step === null) { + this.options.step = 1; + if (this.options.precise && this.useslider) { + this.options.step = (this.options.max - this.options.min) / this.options.split; + } + } + + // create slider if min and max are defined properly + if (this.useslider) { + this.$slider = this.$el.find('#slider'); + this.$slider.slider(this.options); + this.$slider.on('slide', function (event, ui) { + self.value(ui.value); + }); + } else { + this.$el.find('.ui-form-slider-text').css('width', '100%'); + } // backup integer field this.$text = this.$el.find('#text'); - // load slider plugin - this.$slider.slider(this.options); - // add text field event this.$text.on('change', function () { self.value($(this).val()); @@ -39,24 +57,23 @@ // add text field event this.$text.on('keydown', function (event) { var v = event.which; - console.log(v); if (!(v == 8 || v == 9 || v == 13 || v == 37 || v == 39 || v == 189 || (v >= 48 && v <= 57) - || (self.options.step != 1 && $(this).val().indexOf('.') == -1) && v == 190)) { + || (self.options.precise && $(this).val().indexOf('.') == -1) && v == 190)) { event.preventDefault(); } }); - - // add slider event - this.$slider.on('slide', function (event, ui) { - self.value(ui.value); - }); }, // value value : function (new_val) { if (new_val !== undefined) { - // limit - new_val = Math.max(Math.min(new_val, this.options.max), this.options.min); + // apply limit + if (this.options.max !== null) { + new_val = Math.min(new_val, this.options.max); + } + if (this.options.min !== null) { + new_val = Math.max(new_val, this.options.min); + } // trigger on change event if (this.options.onchange) { @@ -64,7 +81,7 @@ } // set values - this.$slider.slider('value', new_val); + this.$slider && this.$slider.slider('value', new_val); this.$text.val(new_val); } This diff is so big that we needed to truncate the remainder. Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.