Merge pull request #5091 from jmchilton/open_large_collections

Render arbitrarily large collections in the UI.
This commit is contained in:
Martin Cech
2017-11-30 19:17:44 -05:00
committed by GitHub
15 changed files with 347 additions and 78 deletions
@@ -52,6 +52,7 @@ var CollectionView = _super.extend(
_queueNewRender: function($newRender, speed) {
speed = speed === undefined ? this.fxSpeed : speed;
var panel = this;
this.handleWarning($newRender);
panel.log("_queueNewRender:", $newRender, speed);
// TODO: jquery@1.12 doesn't change display when the elem has display: flex
@@ -61,6 +62,16 @@ var CollectionView = _super.extend(
panel.trigger("rendered", panel);
},
handleWarning: function($newRender) {
var viewLength = this.views.length;
var elementCount = this.model.get("element_count");
if (elementCount && elementCount !== viewLength) {
var warning = _l(`displaying only ${viewLength} of ${elementCount} items`);
var $warns = $newRender.find(".elements-warning");
$warns.html(`<div class="warningmessagesmall">${warning}</div>`);
}
},
// ------------------------------------------------------------------------ sub-views
/** In this override, use model.getVisibleContents */
_filterCollection: function() {
@@ -149,52 +160,74 @@ var CollectionView = _super.extend(
//------------------------------------------------------------------------------ TEMPLATES
CollectionView.prototype.templates = (() => {
var controlsTemplate = BASE_MVC.wrapTemplate(
[
'<div class="controls">',
'<div class="navigation">',
'<a class="back" href="javascript:void(0)">',
'<span class="fa fa-icon fa-angle-left"></span>',
_l("Back to "),
"<%- view.parentName %>",
"</a>",
"</div>",
'<div class="title">',
'<div class="name"><%- collection.name || collection.element_identifier %></div>',
'<div class="subtitle">',
'<% if( collection.collection_type === "list" ){ %>',
_l("a list of datasets"),
'<% } else if( collection.collection_type === "paired" ){ %>',
_l("a pair of datasets"),
'<% } else if( collection.collection_type === "list:paired" ){ %>',
_l("a list of paired datasets"),
'<% } else if( collection.collection_type === "list:list" ){ %>',
_l("a list of dataset lists"),
"<% } %>",
"</div>",
"</div>",
'<div class="tags-display"></div>',
'<div class="actions">',
'<a class="download-btn icon-btn" ',
'href="<%- view.downloadUrl %>',
'" title="" download="" data-original-title="Download Collection">',
'<span class="fa fa-floppy-o"></span>',
"</a>",
"</div>",
"</div>"
],
"collection"
);
var controlsTemplate = (collection, view) => {
var subtitle = collectionDescription(view.model);
return `
<div class="controls">
<div class="navigation">
<a class="back" href="javascript:void(0)">
<span class="fa fa-icon fa-angle-left"></span>
${_l("Back to ")}
${_.escape(view.parentName)}
</a>
</div>
<div class="title">
<div class="name">${_.escape(collection.name) || _.escape(collection.element_identifier)}</div>
<div class="subtitle">
${subtitle}
</div>
</div>
<div class="elements-warning">
</div>
<div class="tags-display"></div>
<div class="actions">
<a class="download-btn icon-btn" href="${view.downloadUrl}"
title="" download="" data-original-title="Download Collection">
<span class="fa fa-floppy-o"></span>
</a>
</div>
</div>`;
};
return _.extend(_.clone(_super.prototype.templates), {
controls: controlsTemplate
});
})();
function collectionTypeDescription(collection) {
var collectionType = collection.get("collection_type");
var collectionTypeDescription;
if (collectionType == "list") {
collectionTypeDescription = _l("list");
} else if (collectionType == "paired") {
collectionTypeDescription = _l("dataset pair");
} else if (collectionType == "list:paired") {
collectionTypeDescription = _l("list of pairs");
} else {
collectionTypeDescription = _l("nested list");
}
return collectionTypeDescription;
}
function collectionDescription(collection) {
var elementCount = collection.get("element_count");
var itemsDescription = `a ${collectionTypeDescription(collection)}`;
if (elementCount) {
var countDescription;
if (elementCount == 1) {
countDescription = "with 1 item";
} else if (elementCount) {
countDescription = `with ${elementCount} items`;
}
itemsDescription = `${itemsDescription} ${_l(countDescription)}`;
}
return itemsDescription;
}
//==============================================================================
export default {
collectionTypeDescription: collectionTypeDescription,
collectionDescription: collectionDescription,
CollectionView: CollectionView
};
+7 -20
View File
@@ -67,30 +67,17 @@ var HDCAListItemView = _super.extend(
var elementCount = collection.get("element_count");
var jobStateSource = collection.get("job_source_type");
var collectionType = this.model.get("collection_type");
var collectionTypeDescription;
if (collectionType == "list") {
collectionTypeDescription = "list";
} else if (collectionType == "paired") {
collectionTypeDescription = "dataset pair";
} else if (collectionType == "list:paired") {
collectionTypeDescription = "list of pairs";
} else {
collectionTypeDescription = "nested list";
}
var itemsDescription = "";
if (elementCount == 1) {
itemsDescription = ` with 1 item`;
} else if (elementCount) {
itemsDescription = ` with ${elementCount} items`;
}
var collectionTypeDescription = DC_VIEW.collectionTypeDescription(collection);
var simpleDescription = DC_VIEW.collectionDescription(collection);
var jobStatesSummary = collection.jobStatesSummary;
var simpleDescription = `${collectionTypeDescription}${itemsDescription}`;
if (!jobStateSource || jobStateSource == "Job") {
return `a ${simpleDescription}`;
return simpleDescription;
} else if (!jobStatesSummary || !jobStatesSummary.hasDetails()) {
return `
<div class="progress state-progress">
<span class="note">Loading job data for ${collectionTypeDescription}.<span class="blinking">..</span></span>
<span class="note">Loading job data for ${
collectionTypeDescription
}.<span class="blinking">..</span></span>
<div class="progress-bar info" style="width:100%">
</div>`;
} else {
@@ -106,7 +93,7 @@ var HDCAListItemView = _super.extend(
var errorCount = jobStatesSummary.numInError();
return `a ${collectionTypeDescription} with ${errorCount} / ${jobCount} jobs in error`;
} else if (jobStatesSummary.terminal()) {
return `a ${simpleDescription}`;
return simpleDescription;
} else {
var running = jobStatesSummary.states()["running"] || 0;
var ok = jobStatesSummary.states()["ok"] || 0;
@@ -2,6 +2,11 @@ import STATES from "mvc/dataset/states";
import BASE_MVC from "mvc/base-mvc";
import _l from "utils/localization";
var collectionFuzzyCountDefault = 1000;
try {
collectionFuzzyCountDefault = localStorage.getItem("collectionFuzzyCountDefault") || collectionFuzzyCountDefault;
} catch (err) {}
//==============================================================================
/** @class Mixin for HistoryContents content (HDAs, HDCAs).
*/
@@ -58,6 +63,11 @@ var HistoryContentMixin = {
var historyId = this.get("history_id");
var historyContentId = this.get("id");
var url = `${this.urlRoot}${historyId}/contents/${historyContentType}s/${historyContentId}`;
if (historyContentType == "dataset_collection") {
// Don't fetch whole collection - just enought to render outline. Backbone will
// make a detailed request if any datasets are expanded beyond that point.
url = `${url}?view=element-reference&fuzzy_count=${collectionFuzzyCountDefault}`;
}
return url;
},
@@ -6,6 +6,11 @@ import JOB_STATES_MODEL from "mvc/history/job-states-model";
import BASE_MVC from "mvc/base-mvc";
import AJAX_QUEUE from "utils/ajax-queue";
var limitPerPageDefault = 500;
try {
limitPerPageDefault = localStorage.getItem("historyContentsLimitPerPageDefault") || limitPerPageDefault;
} catch (err) {}
//==============================================================================
var _super = CONTROLLED_FETCH_COLLECTION.PaginatedCollection;
/** @class Backbone collection for history content.
@@ -20,10 +25,10 @@ var HistoryContents = _super.extend(BASE_MVC.LoggableMixin).extend({
_logNamespace: "history",
// ........................................................................ set up
limitPerPage: 500,
limitPerPage: limitPerPageDefault,
/** @type {Integer} how many contents per call to fetch when using progressivelyFetchDetails */
limitPerProgressiveFetch: 500,
limitPerProgressiveFetch: limitPerPageDefault,
/** @type {String} order used here and when fetching from server */
order: "hid",
+139 -7
View File
@@ -1,4 +1,5 @@
import logging
import math
from galaxy import exceptions, model, web
from galaxy.util import string_as_bool
@@ -101,8 +102,9 @@ def get_collection_elements(collection, name=""):
return names, hdas
def dictify_dataset_collection_instance(dataset_collection_instance, parent, security, view="element"):
dict_value = dataset_collection_instance.to_dict(view=view)
def dictify_dataset_collection_instance(dataset_collection_instance, parent, security, view="element", fuzzy_count=None):
hdca_view = "element" if view in ["element", "element-reference"] else "collection"
dict_value = dataset_collection_instance.to_dict(view=hdca_view)
encoded_id = security.encode_id(dataset_collection_instance.id)
if isinstance(parent, model.History):
encoded_history_id = security.encode_id(parent.id)
@@ -112,23 +114,66 @@ def dictify_dataset_collection_instance(dataset_collection_instance, parent, sec
encoded_folder_id = security.encode_id(parent.id)
# TODO: Work in progress - this end-point is not right yet...
dict_value['url'] = web.url_for('library_content', library_id=encoded_library_id, id=encoded_id, folder_id=encoded_folder_id)
if view == "element":
if view in ["element", "element-reference"]:
collection = dataset_collection_instance.collection
dict_value['elements'] = [dictify_element(_) for _ in collection.elements]
dict_value['populated'] = collection.populated
rank_fuzzy_counts = gen_rank_fuzzy_counts(collection.collection_type, fuzzy_count)
elements, rest_fuzzy_counts = get_fuzzy_count_elements(collection, rank_fuzzy_counts)
if view == "element":
dict_value['populated'] = collection.populated
element_func = dictify_element
else:
element_func = dictify_element_reference
dict_value['elements'] = [element_func(_, rank_fuzzy_counts=rest_fuzzy_counts) for _ in elements]
security.encode_all_ids(dict_value, recursive=True) # TODO: Use Kyle's recursive formulation of this.
return dict_value
def dictify_element(element):
def dictify_element_reference(element, rank_fuzzy_counts=None):
"""Load minimal details of elements required to show outline of contents in history panel.
History panel can use this reference to expand to full details if individual dataset elements
are clicked.
"""
dictified = element.to_dict(view="element")
element_object = element.element_object
if element_object is not None:
object_detials = dict(
id=element_object.id,
model_class=element_object.__class__.__name__,
)
if element.child_collection:
object_detials["collection_type"] = element_object.collection_type
child_collection = element.child_collection
elements, rest_fuzzy_counts = get_fuzzy_count_elements(child_collection, rank_fuzzy_counts)
# Recursively yield elements for each nested collection...
object_detials["elements"] = [dictify_element_reference(_, rank_fuzzy_counts=rest_fuzzy_counts) for _ in elements]
object_detials["element_count"] = child_collection.element_count
else:
object_detials["state"] = element_object.state
object_detials["hda_ldda"] = 'hda'
object_detials["history_id"] = element_object.history_id
else:
object_detials = None
dictified["object"] = object_detials
return dictified
def dictify_element(element, rank_fuzzy_counts=None):
dictified = element.to_dict(view="element")
element_object = element.element_object
if element_object is not None:
object_detials = element.element_object.to_dict()
if element.child_collection:
child_collection = element.child_collection
elements, rest_fuzzy_counts = get_fuzzy_count_elements(child_collection, rank_fuzzy_counts)
# Recursively yield elements for each nested collection...
child_collection = element.child_collection
object_detials["elements"] = [dictify_element(_) for _ in child_collection.elements]
object_detials["elements"] = [dictify_element(_, rank_fuzzy_counts=rest_fuzzy_counts) for _ in elements]
object_detials["populated"] = child_collection.populated
object_detials["element_count"] = child_collection.element_count
else:
@@ -138,4 +183,91 @@ def dictify_element(element):
return dictified
def get_fuzzy_count_elements(collection, rank_fuzzy_counts):
if rank_fuzzy_counts and rank_fuzzy_counts[0]:
rank_fuzzy_count = rank_fuzzy_counts[0]
elements = collection.elements[0:rank_fuzzy_count]
else:
elements = collection.elements
if rank_fuzzy_counts is not None:
rest_fuzzy_counts = rank_fuzzy_counts[1:]
else:
rest_fuzzy_counts = None
return elements, rest_fuzzy_counts
def gen_rank_fuzzy_counts(collection_type, fuzzy_count=None):
"""Turn a global estimate on elements to return to per nested level based on collection type.
This takes an arbitrary constant and generates an arbitrary constant and is quite messy.
None of this should be relied on as a stable API - it is more of a general guideline to
restrict within broad ranges the amount of objects returned.
>>> def is_around(x, y):
... return y - 1 < x and y + 1 > y
...
>>> gen_rank_fuzzy_counts("list", None)
[None]
>>> gen_rank_fuzzy_counts("list", 500)
[500]
>>> gen_rank_fuzzy_counts("paired", 500)
[2]
>>> gen_rank_fuzzy_counts("list:paired", None)
[None, None]
>>> gen_rank_fuzzy_counts("list:list", 101) # 100 would be edge case at 10 so bump to ensure 11
[11, 11]
>>> ll, pl = gen_rank_fuzzy_counts("list:paired", 100)
>>> pl
2
>>> is_around(ll, 50)
True
>>> pl, ll = gen_rank_fuzzy_counts("paired:list", 100)
>>> pl
2
>>> is_around(ll, 50)
True
>>> gen_rank_fuzzy_counts("list:list:list", 1001)
[11, 11, 11]
>>> l1l, l2l, l3l, pl = gen_rank_fuzzy_counts("list:list:list:paired", 2000)
>>> pl
2
>>> is_around(10, l1l)
True
>>> gen_rank_fuzzy_counts("list:list:list", 1)
[1, 1, 1]
>>> gen_rank_fuzzy_counts("list:list:list", 2)
[2, 2, 2]
>>> gen_rank_fuzzy_counts("paired:paired", 400)
[2, 2]
>>> gen_rank_fuzzy_counts("paired:paired", 5)
[2, 2]
>>> gen_rank_fuzzy_counts("paired:paired", 3)
[2, 2]
>>> gen_rank_fuzzy_counts("paired:paired", 1)
[1, 1]
>>> gen_rank_fuzzy_counts("paired:paired", 2)
[2, 2]
"""
rank_collection_types = collection_type.split(":")
if fuzzy_count is None:
return [None for rt in rank_collection_types]
else:
# This is a list...
paired_count = sum([1 if rt == "paired" else 0 for rt in rank_collection_types])
list_count = len(rank_collection_types) - paired_count
paired_fuzzy_count_mult = 1 if paired_count == 0 else 2 << (paired_count - 1)
list_fuzzy_count_mult = math.floor((fuzzy_count * 1.0) / paired_fuzzy_count_mult)
list_rank_fuzzy_count = int(math.floor(math.pow(list_fuzzy_count_mult, 1.0 / list_count)) + 1) if list_count > 0 else 1.0
pair_rank_fuzzy_count = 2
if list_rank_fuzzy_count > fuzzy_count:
list_rank_fuzzy_count = fuzzy_count
if pair_rank_fuzzy_count > fuzzy_count:
pair_rank_fuzzy_count = fuzzy_count
rank_fuzzy_counts = [pair_rank_fuzzy_count if rt == "paired" else list_rank_fuzzy_count for rt in rank_collection_types]
return rank_fuzzy_counts
__all__ = ('api_payload_to_create_params', 'dictify_dataset_collection_instance')
@@ -127,9 +127,9 @@ class HistoryContentsController(BaseAPIController, UsesLibraryMixin, UsesLibrary
return rval
def __collection_dict(self, trans, dataset_collection_instance, view="collection"):
def __collection_dict(self, trans, dataset_collection_instance, **kwds):
return dictify_dataset_collection_instance(dataset_collection_instance,
security=trans.security, parent=dataset_collection_instance.history, view=view)
security=trans.security, parent=dataset_collection_instance.history, **kwds)
@expose_api_anonymous
def show(self, trans, id, history_id, **kwd):
@@ -145,6 +145,28 @@ class HistoryContentsController(BaseAPIController, UsesLibraryMixin, UsesLibrary
:param id: 'dataset' or 'dataset_collection'
:type history_id: str
:param history_id: encoded id string of the HDA's or HDCA's History
:type view: str
:param view: if fetching a dataset collection - the view style of
the dataset collection to produce.
'collection' returns no element information, 'element'
returns detailed element information for all datasets,
'element-reference' returns a minimal set of information
about datasets (for instance id, type, and state but not
metadata, peek, info, or name). The default is 'element'.
:type fuzzy_count: int
:param fuzzy_count: this value can be used to broadly restrict the magnitude
of the number of elements returned via the API for large
collections. The number of actual elements returned may
be "a bit" more than this number or "a lot" less - varying
on the depth of nesting, balance of nesting at each level,
and size of target collection. The consumer of this API should
not expect a stable number or pre-calculable number of
elements to be produced given this parameter - the only
promise is that this API will not respond with an order
of magnitude more elements estimated with this value.
The UI uses this parameter to fetch a "balanced" concept of
the "start" of large collections at every depth of the
collection.
:rtype: dict
:returns: dictionary containing detailed HDA or HDCA information
@@ -242,7 +264,11 @@ class HistoryContentsController(BaseAPIController, UsesLibraryMixin, UsesLibrary
def __show_dataset_collection(self, trans, id, history_id, **kwd):
dataset_collection_instance = self.__get_accessible_collection(trans, id, history_id)
return self.__collection_dict(trans, dataset_collection_instance, view="element")
view = kwd.get("view", "element")
fuzzy_count = kwd.get("fuzzy_count", None)
if fuzzy_count:
fuzzy_count = int(fuzzy_count)
return self.__collection_dict(trans, dataset_collection_instance, view=view, fuzzy_count=fuzzy_count)
def __get_accessible_collection(self, trans, id, history_id):
return trans.app.dataset_collections_service.get_dataset_collection_instance(
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
define("mvc/collection/collection-view",["exports","mvc/list/list-view","mvc/collection/collection-model","mvc/collection/collection-li","mvc/base-mvc","utils/localization"],function(e,t,l,i,o,a){"use strict";function s(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(e,"__esModule",{value:!0});var n=s(t),c=(s(l),s(i)),d=s(o),r=s(a),p=n.default.ModelListPanel,f=p.extend({_logNamespace:"collections",className:p.prototype.className+" dataset-collection-panel",DatasetDCEViewClass:c.default.DatasetDCEListItemView,modelCollectionKey:"elements",initialize:function(e){p.prototype.initialize.call(this,e),this.linkTarget=e.linkTarget||"_blank",this.hasUser=e.hasUser,this.panelStack=[],this.parentName=e.parentName,this.foldoutStyle=e.foldoutStyle||"foldout",this.downloadUrl=Galaxy.root+"api/dataset_collections/"+this.model.attributes.id+"/download"},getNestedDCDCEViewClass:function(){return c.default.NestedDCDCEListItemView.extend({foldoutPanelClass:f})},_queueNewRender:function(e,t){t=void 0===t?this.fxSpeed:t;var l=this;l.log("_queueNewRender:",e,t),l._swapNewRender(e),l.trigger("rendered",l)},_filterCollection:function(){return this.model.getVisibleContents()},_getItemViewClass:function(e){switch(e.get("element_type")){case"hda":return this.DatasetDCEViewClass;case"dataset_collection":return this.getNestedDCDCEViewClass()}throw new TypeError("Unknown element type:",e.get("element_type"))},_getItemViewOptions:function(e){var t=p.prototype._getItemViewOptions.call(this,e);return _.extend(t,{linkTarget:this.linkTarget,hasUser:this.hasUser,foldoutStyle:this.foldoutStyle})},_setUpItemViewListeners:function(e){var t=this;return p.prototype._setUpItemViewListeners.call(t,e),t.listenTo(e,{"expanded:drilldown":function(e,t){this._expandDrilldownPanel(t)},"collapsed:drilldown":function(e,t){this._collapseDrilldownPanel(t)}}),this},_expandDrilldownPanel:function(e){this.panelStack.push(e),this.$("> .controls").add(this.$list()).hide(),e.parentName=this.model.get("name"),this.$el.append(e.render().$el)},_collapseDrilldownPanel:function(e){this.panelStack.pop(),this.render()},events:{"click .navigation .back":"close"},close:function(e){this.remove(),this.trigger("close")},toString:function(){return"CollectionView("+(this.model?this.model.get("name"):"")+")"}});f.prototype.templates=function(){var e=d.default.wrapTemplate(['<div class="controls">','<div class="navigation">','<a class="back" href="javascript:void(0)">','<span class="fa fa-icon fa-angle-left"></span>',(0,r.default)("Back to "),"<%- view.parentName %>","</a>","</div>",'<div class="title">','<div class="name"><%- collection.name || collection.element_identifier %></div>','<div class="subtitle">','<% if( collection.collection_type === "list" ){ %>',(0,r.default)("a list of datasets"),'<% } else if( collection.collection_type === "paired" ){ %>',(0,r.default)("a pair of datasets"),'<% } else if( collection.collection_type === "list:paired" ){ %>',(0,r.default)("a list of paired datasets"),'<% } else if( collection.collection_type === "list:list" ){ %>',(0,r.default)("a list of dataset lists"),"<% } %>","</div>","</div>",'<div class="tags-display"></div>','<div class="actions">','<a class="download-btn icon-btn" ','href="<%- view.downloadUrl %>','" title="" download="" data-original-title="Download Collection">','<span class="fa fa-floppy-o"></span>',"</a>","</div>","</div>"],"collection");return _.extend(_.clone(p.prototype.templates),{controls:e})}(),e.default={CollectionView:f}});
define("mvc/collection/collection-view",["exports","mvc/list/list-view","mvc/collection/collection-model","mvc/collection/collection-li","mvc/base-mvc","utils/localization"],function(e,t,n,i,l,a){"use strict";function s(e){return e&&e.__esModule?e:{default:e}}function o(e){var t=e.get("collection_type");return"list"==t?(0,p.default)("list"):"paired"==t?(0,p.default)("dataset pair"):"list:paired"==t?(0,p.default)("list of pairs"):(0,p.default)("nested list")}function d(e){var t=e.get("element_count"),n="a "+o(e);if(t){var i;1==t?i="with 1 item":t&&(i="with "+t+" items"),n=n+" "+(0,p.default)(i)}return n}Object.defineProperty(e,"__esModule",{value:!0});var c=s(t),r=(s(n),s(i)),p=(s(l),s(a)),u=c.default.ModelListPanel,f=u.extend({_logNamespace:"collections",className:u.prototype.className+" dataset-collection-panel",DatasetDCEViewClass:r.default.DatasetDCEListItemView,modelCollectionKey:"elements",initialize:function(e){u.prototype.initialize.call(this,e),this.linkTarget=e.linkTarget||"_blank",this.hasUser=e.hasUser,this.panelStack=[],this.parentName=e.parentName,this.foldoutStyle=e.foldoutStyle||"foldout",this.downloadUrl=Galaxy.root+"api/dataset_collections/"+this.model.attributes.id+"/download"},getNestedDCDCEViewClass:function(){return r.default.NestedDCDCEListItemView.extend({foldoutPanelClass:f})},_queueNewRender:function(e,t){t=void 0===t?this.fxSpeed:t;var n=this;this.handleWarning(e),n.log("_queueNewRender:",e,t),n._swapNewRender(e),n.trigger("rendered",n)},handleWarning:function(e){var t=this.views.length,n=this.model.get("element_count");if(n&&n!==t){var i=(0,p.default)("displaying only "+t+" of "+n+" items");e.find(".elements-warning").html('<div class="warningmessagesmall">'+i+"</div>")}},_filterCollection:function(){return this.model.getVisibleContents()},_getItemViewClass:function(e){switch(e.get("element_type")){case"hda":return this.DatasetDCEViewClass;case"dataset_collection":return this.getNestedDCDCEViewClass()}throw new TypeError("Unknown element type:",e.get("element_type"))},_getItemViewOptions:function(e){var t=u.prototype._getItemViewOptions.call(this,e);return _.extend(t,{linkTarget:this.linkTarget,hasUser:this.hasUser,foldoutStyle:this.foldoutStyle})},_setUpItemViewListeners:function(e){var t=this;return u.prototype._setUpItemViewListeners.call(t,e),t.listenTo(e,{"expanded:drilldown":function(e,t){this._expandDrilldownPanel(t)},"collapsed:drilldown":function(e,t){this._collapseDrilldownPanel(t)}}),this},_expandDrilldownPanel:function(e){this.panelStack.push(e),this.$("> .controls").add(this.$list()).hide(),e.parentName=this.model.get("name"),this.$el.append(e.render().$el)},_collapseDrilldownPanel:function(e){this.panelStack.pop(),this.render()},events:{"click .navigation .back":"close"},close:function(e){this.remove(),this.trigger("close")},toString:function(){return"CollectionView("+(this.model?this.model.get("name"):"")+")"}});f.prototype.templates=_.extend(_.clone(u.prototype.templates),{controls:function(e,t){var n=d(t.model);return'\n <div class="controls">\n <div class="navigation">\n <a class="back" href="javascript:void(0)">\n <span class="fa fa-icon fa-angle-left"></span>\n '+(0,p.default)("Back to ")+"\n "+_.escape(t.parentName)+'\n </a>\n </div>\n <div class="title">\n <div class="name">'+(_.escape(e.name)||_.escape(e.element_identifier))+'</div>\n <div class="subtitle">\n '+n+'\n </div>\n </div>\n <div class="elements-warning">\n </div>\n <div class="tags-display"></div>\n <div class="actions">\n <a class="download-btn icon-btn" href="'+t.downloadUrl+'"\n title="" download="" data-original-title="Download Collection">\n <span class="fa fa-floppy-o"></span>\n </a>\n </div>\n </div>'}}),e.default={collectionTypeDescription:o,collectionDescription:d,CollectionView:f}});
+1 -1
View File
@@ -1 +1 @@
define("mvc/history/hdca-li",["exports","mvc/dataset/states","mvc/collection/collection-li","mvc/collection/collection-view","mvc/base-mvc","mvc/history/history-item-li","utils/localization"],function(t,e,s,n,i,a,r){"use strict";function o(t){return t&&t.__esModule?t:{default:t}}Object.defineProperty(t,"__esModule",{value:!0});var l=o(e),d=o(s),c=o(n),p=(o(i),o(a)),u=o(r),m=d.default.DCListItemView,h=m.extend({className:m.prototype.className+" history-content",_setUpListeners:function(){var t=this;m.prototype._setUpListeners.call(this);var e=function(e,s){t.render()};this.model.jobStatesSummary&&this.listenTo(this.model.jobStatesSummary,"change",e),this.listenTo(this.model,{"change:tags change:visible change:state":e})},_getFoldoutPanelClass:function(){return c.default.CollectionView},_swapNewRender:function(t){m.prototype._swapNewRender.call(this,t);var e,s=this.model.jobStatesSummary;e=s?s.new()?"loading":s.errored()?"error":s.terminal()?"ok":s.running()?"running":"queued":this.model.get("job_source_id")?"loading":this.model.get("populated_state")?l.default.OK:l.default.RUNNING,this.$el.addClass("state-"+e);var n=this.stateDescription();return this.$(".state-description").html(n),this.$el},stateDescription:function(){var t,e=this.model,s=e.get("element_count"),n=e.get("job_source_type"),i=this.model.get("collection_type");t="list"==i?"list":"paired"==i?"dataset pair":"list:paired"==i?"list of pairs":"nested list";var a="";1==s?a=" with 1 item":s&&(a=" with "+s+" items");var r=e.jobStatesSummary,o=""+t+a;if(n&&"Job"!=n){if(r&&r.hasDetails()){var l=r.new(),d=l?null:r.jobCount();if(l)return'\n <div class="progress state-progress">\n <span class="note">Creating jobs.<span class="blinking">..</span></span>\n <div class="progress-bar info" style="width:100%">\n </div>';if(r.errored())return"a "+t+" with "+r.numInError()+" / "+d+" jobs in error";if(r.terminal())return"a "+o;var c=r.states().running||0,p=(r.states().ok||0)/(1*d),u=c/(1*d),m=1-p-u;return'\n <div class="progress state-progress">\n <span class="note">'+(d&&d>1?d+" jobs":"a job")+" generating a "+t+'</span>\n <div class="progress-bar ok" style="width:'+100*p+'%"></div>\n <div class="progress-bar running" style="width:'+100*u+'%"></div>\n <div class="progress-bar new" style="width:'+100*m+'%">\n </div>'}return'\n <div class="progress state-progress">\n <span class="note">Loading job data for '+t+'.<span class="blinking">..</span></span>\n <div class="progress-bar info" style="width:100%">\n </div>'}return"a "+o},toString:function(){return"HDCAListItemView("+(this.model?""+this.model:"(no model)")+")"}});h.prototype.templates=function(){var t=_.extend({},m.prototype.templates.warnings,{hidden:function(t){t.visible||(0,u.default)("This collection has been hidden")}});return _.extend({},m.prototype.templates,{warnings:t,titleBar:function(t){return'\n <div class="title-bar clear" tabindex="0">\n <span class="state-icon"></span>\n <div class="title">\n <span class="hid">'+t.hid+'</span>\n <span class="name">'+_.escape(t.name)+'</span>\n </div>\n <div class="state-description">\n </div>\n '+p.default.nametagTemplate(t)+"\n </div>\n "}})}(),t.default={HDCAListItemView:h}});
define("mvc/history/hdca-li",["exports","mvc/dataset/states","mvc/collection/collection-li","mvc/collection/collection-view","mvc/base-mvc","mvc/history/history-item-li","utils/localization"],function(e,t,s,n,i,a,o){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(e,"__esModule",{value:!0});var l=r(t),c=r(s),d=r(n),p=(r(i),r(a)),u=r(o),m=c.default.DCListItemView,h=m.extend({className:m.prototype.className+" history-content",_setUpListeners:function(){var e=this;m.prototype._setUpListeners.call(this);var t=function(t,s){e.render()};this.model.jobStatesSummary&&this.listenTo(this.model.jobStatesSummary,"change",t),this.listenTo(this.model,{"change:tags change:visible change:state":t})},_getFoldoutPanelClass:function(){return d.default.CollectionView},_swapNewRender:function(e){m.prototype._swapNewRender.call(this,e);var t,s=this.model.jobStatesSummary;t=s?s.new()?"loading":s.errored()?"error":s.terminal()?"ok":s.running()?"running":"queued":this.model.get("job_source_id")?"loading":this.model.get("populated_state")?l.default.OK:l.default.RUNNING,this.$el.addClass("state-"+t);var n=this.stateDescription();return this.$(".state-description").html(n),this.$el},stateDescription:function(){var e=this.model,t=(e.get("element_count"),e.get("job_source_type")),s=(this.model.get("collection_type"),d.default.collectionTypeDescription(e)),n=d.default.collectionDescription(e),i=e.jobStatesSummary;if(t&&"Job"!=t){if(i&&i.hasDetails()){var a=i.new(),o=a?null:i.jobCount();if(a)return'\n <div class="progress state-progress">\n <span class="note">Creating jobs.<span class="blinking">..</span></span>\n <div class="progress-bar info" style="width:100%">\n </div>';if(i.errored())return"a "+s+" with "+i.numInError()+" / "+o+" jobs in error";if(i.terminal())return n;var r=i.states().running||0,l=(i.states().ok||0)/(1*o),c=r/(1*o),p=1-l-c;return'\n <div class="progress state-progress">\n <span class="note">'+(o&&o>1?o+" jobs":"a job")+" generating a "+s+'</span>\n <div class="progress-bar ok" style="width:'+100*l+'%"></div>\n <div class="progress-bar running" style="width:'+100*c+'%"></div>\n <div class="progress-bar new" style="width:'+100*p+'%">\n </div>'}return'\n <div class="progress state-progress">\n <span class="note">Loading job data for '+s+'.<span class="blinking">..</span></span>\n <div class="progress-bar info" style="width:100%">\n </div>'}return n},toString:function(){return"HDCAListItemView("+(this.model?""+this.model:"(no model)")+")"}});h.prototype.templates=function(){var e=_.extend({},m.prototype.templates.warnings,{hidden:function(e){e.visible||(0,u.default)("This collection has been hidden")}});return _.extend({},m.prototype.templates,{warnings:e,titleBar:function(e){return'\n <div class="title-bar clear" tabindex="0">\n <span class="state-icon"></span>\n <div class="title">\n <span class="hid">'+e.hid+'</span>\n <span class="name">'+_.escape(e.name)+'</span>\n </div>\n <div class="state-description">\n </div>\n '+p.default.nametagTemplate(e)+"\n </div>\n "}})}(),e.default={HDCAListItemView:h}});
@@ -1 +1 @@
define("mvc/history/history-content-model",["exports","mvc/dataset/states","mvc/base-mvc","utils/localization"],function(t,e,i,s){"use strict";function n(t){return t&&t.__esModule?t:{default:t}}Object.defineProperty(t,"__esModule",{value:!0});n(e),n(i),n(s);var r={defaults:{history_id:null,history_content_type:null,hid:null,visible:!0},idAttribute:"type_id",hidden:function(){return!this.get("visible")},isVisible:function(t,e){var i=!0;return t||!this.get("deleted")&&!this.get("purged")||(i=!1),e||this.get("visible")||(i=!1),i},urlRoot:Galaxy.root+"api/histories/",url:function(){var t=this.get("history_content_type"),e=this.get("history_id"),i=this.get("id"),s=""+this.urlRoot+e+"/contents/"+t+"s/"+i;return s},hide:function(t){return this.get("visible")?this.save({visible:!1},t):jQuery.when()},unhide:function(t){return this.get("visible")?jQuery.when():this.save({visible:!0},t)},toString:function(){return[this.get("type_id"),this.get("hid"),this.get("name")].join(":")}};t.default={HistoryContentMixin:r}});
define("mvc/history/history-content-model",["exports","mvc/dataset/states","mvc/base-mvc","utils/localization"],function(t,e,i,n){"use strict";function s(t){return t&&t.__esModule?t:{default:t}}Object.defineProperty(t,"__esModule",{value:!0});s(e),s(i),s(n);var o=1e3;try{o=localStorage.getItem("collectionFuzzyCountDefault")||o}catch(t){}var r={defaults:{history_id:null,history_content_type:null,hid:null,visible:!0},idAttribute:"type_id",hidden:function(){return!this.get("visible")},isVisible:function(t,e){var i=!0;return t||!this.get("deleted")&&!this.get("purged")||(i=!1),e||this.get("visible")||(i=!1),i},urlRoot:Galaxy.root+"api/histories/",url:function(){var t=this.get("history_content_type"),e=this.get("history_id"),i=this.get("id"),n=""+this.urlRoot+e+"/contents/"+t+"s/"+i;return"dataset_collection"==t&&(n=n+"?view=element-reference&fuzzy_count="+o),n},hide:function(t){return this.get("visible")?this.save({visible:!1},t):jQuery.when()},unhide:function(t){return this.get("visible")?jQuery.when():this.save({visible:!0},t)},toString:function(){return[this.get("type_id"),this.get("hid"),this.get("name")].join(":")}};t.default={HistoryContentMixin:r}});
File diff suppressed because one or more lines are too long
+8
View File
@@ -154,6 +154,14 @@ class NavigatesGalaxy(HasDriver):
def switch_to_main_panel(self):
self.driver.switch_to.frame("galaxy_main")
@contextlib.contextmanager
def local_storage(self, key, value):
self.driver.execute_script('''window.localStorage.setItem("%s", %s);''' % (key, value))
try:
yield
finally:
self.driver.execute_script('''window.localStorage.removeItem("%s");''' % key)
@contextlib.contextmanager
def main_panel(self):
try:
+9 -1
View File
@@ -110,8 +110,9 @@ history_panel:
_: '.list-panel.dataset-collection-panel'
back: '.navigation .back'
title: '.dataset-collection-panel .controls .title .editable-text'
title_input: '.dataset-collection-panel .controls .title input'
title_input: '.dataset-collection-panel .controls .title input'
subtitle: '.dataset-collection-panel .controls .title .subtitle'
elements_warning: '.dataset-collection-panel .controls .elements-warning'
tag_area_input: '.controls .tags-display .tags-input input'
list_items: '.dataset-collection-panel .list-items .list-item'
@@ -139,6 +140,13 @@ history_panel:
options_menu: '#history-options-button-menu'
multi_view_button: '#history-view-multi-button'
pagination_pages: '.list-pagination .pages'
pagination_pages_options: '.list-pagination .pages option'
pagination_pages_selected_option: '.list-pagination .pages option:checked'
pagination_next: '.list-pagination button.next'
pagination_previous: '.list-pagination button.prev'
text:
tooltip_name: 'Click to rename history'
new_name: 'Unnamed history'
@@ -220,6 +220,21 @@ class HistoryPanelCollectionsTestCase(SeleniumTestCase):
self._click_and_wait_for_collection_view(collection_hid)
self.screenshot("history_panel_collection_view_list_list")
@selenium_test
def test_limiting_collection_rendering(self):
history_id = self.current_history_id()
collection = self.dataset_collection_populator.create_list_in_history(history_id, contents=["0", "1", "0", "1"]).json()
collection_hid = collection["hid"]
with self.local_storage("collectionFuzzyCountDefault", 2):
self.home()
self.history_panel_wait_for_hid_state(collection_hid, "ok")
self._click_and_wait_for_collection_view(collection_hid)
self.screenshot("history_panel_collection_view_limiting")
warning_text = self.components.history_panel.collection_view.elements_warning.wait_for_text()
assert "only 2 of 4 items" in warning_text, warning_text
def _generate_partially_failed_collection_with_input(self):
history_id = self.current_history_id()
input_collection = self.dataset_collection_populator.create_list_in_history(history_id, contents=["0", "1", "0", "1"]).json()
@@ -0,0 +1,45 @@
from base.populators import flakey
from .framework import (
selenium_test,
SeleniumTestCase
)
class HistoryPanelPaginationTestCase(SeleniumTestCase):
ensure_registered = True
@selenium_test
@flakey # The next button doesn't always work - maybe a delay in JS callback registering for that.
def test_pagination(self):
history_id = self.current_history_id()
self.dataset_populator.new_dataset(history_id, content='1\t2\t3', name="data1")
self.dataset_populator.new_dataset(history_id, content='2\t3\t4', name="data2")
self.dataset_populator.new_dataset(history_id, content='3\t4\t5', name="data3")
self.dataset_populator.new_dataset(history_id, content='4\t5\t6', name="data4")
self.dataset_populator.new_dataset(history_id, content='5\t6\t7', name="data5")
self.home()
for hid in [1, 2, 3, 4, 5]:
self.history_panel_wait_for_hid_state(hid, "ok")
with self.local_storage("historyContentsLimitPerPageDefault", 3):
self.home()
self.history_panel_wait_for_hid_state(5, "ok")
self.screenshot("history_panel_pagination_initial")
pagination_option_text = self.components.history_panel.pagination_pages_selected_option.wait_for_text()
assert "1st of 2 pages" in pagination_option_text
self.components.history_panel.pagination_pages.wait_for_and_click()
self.screenshot("history_panel_pagination_pages_drop_down")
self.components.history_panel.pagination_next.wait_for_and_click()
self.sleep_for(self.wait_types.UX_TRANSITION)
self.screenshot("history_panel_pagination_second")
pagination_option_text = self.components.history_panel.pagination_pages_selected_option.wait_for_text()
assert "2nd of 2 pages" in pagination_option_text
self.components.history_panel.pagination_previous.wait_for_and_click()
self.sleep_for(self.wait_types.UX_TRANSITION)
pagination_option_text = self.components.history_panel.pagination_pages_selected_option.wait_for_text()
assert "1st of 2 pages" in pagination_option_text