Remove legacy collection and dataset handling

This commit is contained in:
guerler
2022-09-21 23:08:35 -04:00
parent 04453800d3
commit 7da1c86af9
10 changed files with 0 additions and 3114 deletions
@@ -1,115 +0,0 @@
import $ from "jquery";
import _ from "underscore";
import DC_LI from "mvc/collection/collection-li";
import DATASET_LI_EDIT from "mvc/dataset/dataset-li-edit";
//==============================================================================
var DCListItemView = DC_LI.DCListItemView;
/** @class Edit view for DatasetCollection.
*/
var DCListItemEdit = DCListItemView.extend(
/** @lends DCListItemEdit.prototype */ {
/** override to add linkTarget */
initialize: function (attributes) {
DCListItemView.prototype.initialize.call(this, attributes);
},
// ......................................................................... misc
/** String representation */
toString: function () {
var modelString = this.model ? `${this.model}` : "(no model)";
return `DCListItemEdit(${modelString})`;
},
}
);
//==============================================================================
var DCEListItemView = DC_LI.DCEListItemView;
/** @class Read only view for DatasetCollectionElement.
*/
var DCEListItemEdit = DCEListItemView.extend(
/** @lends DCEListItemEdit.prototype */ {
//TODO: this might be expendable - compacted with HDAListItemView
/** set up */
initialize: function (attributes) {
DCEListItemView.prototype.initialize.call(this, attributes);
},
// ......................................................................... misc
/** String representation */
toString: function () {
var modelString = this.model ? `${this.model}` : "(no model)";
return `DCEListItemEdit(${modelString})`;
},
}
);
//==============================================================================
// NOTE: this does not inherit from DatasetDCEListItemView as you would expect
//TODO: but should - if we can find something simpler than using diamond
/** @class Editable view for a DatasetCollectionElement that is also an DatasetAssociation
* (a dataset contained in a dataset collection).
*/
var DatasetDCEListItemEdit = DATASET_LI_EDIT.DatasetListItemEdit.extend(
/** @lends DatasetDCEListItemEdit.prototype */ {
/** set up */
initialize: function (attributes) {
DATASET_LI_EDIT.DatasetListItemEdit.prototype.initialize.call(this, attributes);
},
// NOTE: this does not inherit from DatasetDCEListItemView - so we duplicate this here
//TODO: fix
/** In this override, only get details if in the ready state.
* Note: fetch with no 'change' event triggering to prevent automatic rendering.
*/
_fetchModelDetails: function () {
var view = this;
if (view.model.inReadyState() && !view.model.hasDetails()) {
return view.model.fetch({ silent: true });
}
return $.when();
},
/** Override to remove delete button */
_renderDeleteButton: function () {
return null;
},
// ......................................................................... misc
/** String representation */
toString: function () {
var modelString = this.model ? `${this.model}` : "(no model)";
return `DatasetDCEListItemEdit(${modelString})`;
},
}
);
// ............................................................................ TEMPLATES
/** underscore templates */
DatasetDCEListItemEdit.prototype.templates = (() =>
_.extend({}, DATASET_LI_EDIT.DatasetListItemEdit.prototype.templates, {
titleBar: DC_LI.DatasetDCEListItemView.prototype.templates.titleBar,
}))();
//==============================================================================
/** @class Read only view for a DatasetCollectionElement that is also a DatasetCollection
* (a nested DC).
*/
var NestedDCDCEListItemEdit = DC_LI.NestedDCDCEListItemView.extend(
/** @lends NestedDCDCEListItemEdit.prototype */ {
/** String representation */
toString: function () {
var modelString = this.model ? `${this.model}` : "(no model)";
return `NestedDCDCEListItemEdit(${modelString})`;
},
}
);
//==============================================================================
export default {
DCListItemEdit: DCListItemEdit,
DCEListItemEdit: DCEListItemEdit,
DatasetDCEListItemEdit: DatasetDCEListItemEdit,
NestedDCDCEListItemEdit: NestedDCDCEListItemEdit,
};
-277
View File
@@ -1,277 +0,0 @@
import _ from "underscore";
import $ from "jquery";
import { ListItemView, FoldoutListItemView } from "mvc/list/list-item";
import { DatasetListItemView } from "mvc/dataset/dataset-li";
import BASE_MVC from "mvc/base-mvc";
import _l from "utils/localization";
/** @class Read only view for DatasetCollection.
*/
var DCListItemView = FoldoutListItemView.extend(
/** @lends DCListItemView.prototype */ {
className: `${FoldoutListItemView.prototype.className} dataset-collection`,
id: function () {
return ["dataset_collection", this.model.get("id")].join("-");
},
/** override to add linkTarget */
initialize: function (attributes) {
this.linkTarget = attributes.linkTarget || "_blank";
this.hasUser = attributes.hasUser;
FoldoutListItemView.prototype.initialize.call(this, attributes);
},
/** event listeners */
_setUpListeners: function () {
FoldoutListItemView.prototype._setUpListeners.call(this);
this.listenTo(this.model, "change", function (model, options) {
// if the model has changed deletion status render it entirely
if (_.has(model.changed, "deleted")) {
this.render();
// if the model has been decorated after the fact with the element count,
// render the subtitle where the count is displayed
} else if (_.has(model.changed, "element_count")) {
this.$("> .title-bar .subtitle").replaceWith(this._renderSubtitle());
}
});
},
// ......................................................................... rendering
/** render a subtitle to show the user what sort of collection this is */
_renderSubtitle: function () {
return $(this.templates.subtitle(this.model.toJSON(), this));
},
// ......................................................................... foldout
/** override to add linktarget to sub-panel */
_getFoldoutPanelOptions: function () {
var options = FoldoutListItemView.prototype._getFoldoutPanelOptions.call(this);
return _.extend(options, {
linkTarget: this.linkTarget,
hasUser: this.hasUser,
});
},
/** override to not catch sub-panel selectors */
$selector: function () {
return this.$("> .selector");
},
// ......................................................................... misc
/** String representation */
toString: function () {
var modelString = this.model ? `${this.model}` : "(no model)";
return `DCListItemView(${modelString})`;
},
}
);
// ............................................................................ TEMPLATES
/** underscore templates */
DCListItemView.prototype.templates = (() => {
var warnings = _.extend({}, FoldoutListItemView.prototype.templates.warnings, {
error: BASE_MVC.wrapTemplate([
// error during index fetch - show error on dataset
"<% if( model.error ){ %>",
'<div class="errormessagesmall">',
_l("There was an error getting the data for this collection"),
": <%- model.error %>",
"</div>",
"<% } %>",
]),
purged: BASE_MVC.wrapTemplate([
"<% if( model.purged ){ %>",
'<div class="purged-msg warningmessagesmall">',
_l("This collection has been deleted and removed from disk"),
"</div>",
"<% } %>",
]),
deleted: BASE_MVC.wrapTemplate([
// deleted not purged
"<% if( model.deleted && !model.purged ){ %>",
'<div class="deleted-msg warningmessagesmall">',
_l("This collection has been deleted"),
"</div>",
"<% } %>",
]),
});
// use element identifier
var titleBarTemplate = BASE_MVC.wrapTemplate(
[
'<div class="title-bar clear" tabindex="0">',
'<div class="title">',
'<span class="name"><%- collection.element_identifier || collection.name %></span>',
"</div>",
'<div class="subtitle"></div>',
"</div>",
],
"collection"
);
// use element identifier
var subtitleTemplate = BASE_MVC.wrapTemplate(
[
'<div class="subtitle">',
'<% var countText = collection.element_count? ( collection.element_count + " " ) : ""; %>',
'<% if( collection.collection_type === "list" ){ %>',
_l("a list of <%- countText %>datasets"),
'<% } else if( collection.collection_type === "paired" ){ %>',
_l("a pair of datasets"),
'<% } else if( collection.collection_type === "list:paired" ){ %>',
_l("a list of <%- countText %>dataset pairs"),
'<% } else if( collection.collection_type === "list:list" ){ %>',
_l("a list of <%- countText %>dataset lists"),
"<% } %>",
"</div>",
],
"collection"
);
return _.extend({}, FoldoutListItemView.prototype.templates, {
warnings: warnings,
titleBar: titleBarTemplate,
subtitle: subtitleTemplate,
});
})();
//==============================================================================
/** @class Read only view for DatasetCollectionElement.
*/
var DCEListItemView = ListItemView.extend(
/** @lends DCEListItemView.prototype */ {
/** add the DCE class to the list item */
className: `${ListItemView.prototype.className} dataset-collection-element`,
/** set up */
initialize: function (attributes) {
if (attributes.logger) {
this.logger = this.model.logger = attributes.logger;
}
this.log("DCEListItemView.initialize:", attributes);
ListItemView.prototype.initialize.call(this, attributes);
},
// ......................................................................... misc
/** String representation */
toString: function () {
var modelString = this.model ? `${this.model}` : "(no model)";
return `DCEListItemView(${modelString})`;
},
}
);
// ............................................................................ TEMPLATES
/** underscore templates */
DCEListItemView.prototype.templates = (() => {
// use the element identifier here - since that will persist and the user will need it
var titleBarTemplate = BASE_MVC.wrapTemplate(
[
'<div class="title-bar clear" tabindex="0">',
'<div class="title">',
'<span class="name"><%- element.element_identifier %></span>',
"</div>",
'<div class="subtitle"></div>',
"</div>",
],
"element"
);
return _.extend({}, ListItemView.prototype.templates, {
titleBar: titleBarTemplate,
});
})();
//==============================================================================
/** @class Read only view for a DatasetCollectionElement that is also an DatasetAssociation
* (a dataset contained in a dataset collection).
*/
var DatasetDCEListItemView = DatasetListItemView.extend(
/** @lends DatasetDCEListItemView.prototype */ {
className: `${DatasetListItemView.prototype.className} dataset-collection-element`,
/** set up */
initialize: function (attributes) {
if (attributes.logger) {
this.logger = this.model.logger = attributes.logger;
}
this.log("DatasetDCEListItemView.initialize:", attributes);
DatasetListItemView.prototype.initialize.call(this, attributes);
},
/** In this override, only get details if in the ready state.
* Note: fetch with no 'change' event triggering to prevent automatic rendering.
*/
_fetchModelDetails: function () {
var view = this;
if (view.model.inReadyState() && !view.model.hasDetails()) {
return view.model.fetch({ silent: true });
}
return $.when();
},
// ......................................................................... misc
/** String representation */
toString: function () {
var modelString = this.model ? `${this.model}` : "(no model)";
return `DatasetDCEListItemView(${modelString})`;
},
}
);
// ............................................................................ TEMPLATES
/** underscore templates */
DatasetDCEListItemView.prototype.templates = (() => {
// use the element identifier here and not the dataset name
//TODO:?? can we steal the DCE titlebar?
var titleBarTemplate = BASE_MVC.wrapTemplate(
[
'<div class="title-bar clear" tabindex="0">',
'<span class="state-icon"></span>',
'<div class="title">',
'<span class="name"><%- element.element_identifier %></span>',
"</div>",
"</div>",
],
"element"
);
return _.extend({}, DatasetListItemView.prototype.templates, {
titleBar: titleBarTemplate,
});
})();
//==============================================================================
/** @class Read only view for a DatasetCollectionElement that is also a DatasetCollection
* (a nested DC).
*/
var NestedDCDCEListItemView = DCListItemView.extend(
/** @lends NestedDCDCEListItemView.prototype */ {
className: `${DCListItemView.prototype.className} dataset-collection-element`,
/** In this override, add the state as a class for use with state-based CSS */
_swapNewRender: function ($newRender) {
DCListItemView.prototype._swapNewRender.call(this, $newRender);
var state = this.model.get("state") || "ok";
this.$el.addClass(`state-${state}`);
this.$el.attr("data-state", state);
return this.$el;
},
// ......................................................................... misc
/** String representation */
toString: function () {
var modelString = this.model ? `${this.model}` : "(no model)";
return `NestedDCDCEListItemView(${modelString})`;
},
}
);
//==============================================================================
export default {
DCListItemView: DCListItemView,
DCEListItemView: DCEListItemView,
DatasetDCEListItemView: DatasetDCEListItemView,
NestedDCDCEListItemView: NestedDCDCEListItemView,
};
@@ -1,396 +0,0 @@
import $ from "jquery";
import _ from "underscore";
import Backbone from "backbone";
import { getAppRoot } from "onload/loadConfig";
import DATASET_MODEL from "mvc/dataset/dataset-model";
import BASE_MVC from "mvc/base-mvc";
import Utils from "utils/utils";
//==============================================================================
/*
Notes:
Terminology:
DatasetCollection/DC : a container of datasets or nested DatasetCollections
Element/DatasetCollectionElement/DCE : an item contained in a DatasetCollection
HistoryDatasetCollectionAssociation/HDCA: a DatasetCollection contained in a history
This all seems too complex unfortunately:
- Terminology collision between DatasetCollections (DCs) and Backbone Collections.
- In the DatasetCollections API JSON, DC Elements use a 'Has A' stucture to *contain*
either a dataset or a nested DC. This would make the hierarchy much taller. I've
decided to merge the contained JSON with the DC element json - making the 'has a'
relation into an 'is a' relation. This seems simpler to me and allowed a lot of
DRY in both models and views, but may make tracking or tracing within these models
more difficult (since DatasetCollectionElements are now *also* DatasetAssociations
or DatasetCollections (nested)). This also violates the rule of thumb about
favoring aggregation over inheritance.
- Currently, there are three DatasetCollection subclasses: List, Pair, and ListPaired.
These each should a) be usable on their own, b) be usable in the context of
nesting within a collection model (at least in the case of ListPaired), and
c) be usable within the context of other container models (like History or
LibraryFolder, etc.). I've tried to separate/extract classes in order to
handle those three situations, but it's proven difficult to do in a simple,
readable manner.
- Ideally, histories and libraries would inherit from the same server models as
dataset collections do since they are (in essence) dataset collections themselves -
making the whole nested structure simpler. This would be a large, error-prone
refactoring and migration.
Many of the classes and heirarchy are meant as extension points so, while the
relations and flow may be difficult to understand initially, they'll allow us to
handle the growth or flux dataset collection in the future (w/o actually implementing
any YAGNI).
*/
//_________________________________________________________________________________________________ ELEMENTS
/** @class mixin for Dataset collection elements.
* When collection elements are passed from the API, the underlying element is
* in a sub-object 'object' (IOW, a DCE representing an HDA will have HDA json in element.object).
* This mixin uses the constructor and parse methods to merge that JSON with the DCE attribtues
* effectively changing a DCE from a container to a subclass (has a --> is a).
*/
var DatasetCollectionElementMixin = {
/** default attributes used by elements in a dataset collection */
defaults: {
model_class: "DatasetCollectionElement",
element_identifier: null,
element_index: null,
element_type: null,
},
/** merge the attributes of the sub-object 'object' into this model */
_mergeObject: function (attributes) {
// Don't let the dataset ID replace the DCE's ID so record it as the
// element_id and when fetching dataset details below use the element_id
// instead of this.id.
const object = attributes.object;
let elementId = this.elementId;
if (object) {
elementId = attributes.object.id;
delete attributes.object.id;
}
_.extend(attributes, attributes.object, {
element_id: elementId,
});
delete attributes.object;
return attributes;
},
/** override to merge this.object into this */
constructor: function (attributes, options) {
// console.debug( '\t DatasetCollectionElement.constructor:', attributes, options );
attributes = this._mergeObject(attributes);
this.idAttribute = "element_id";
Backbone.Model.apply(this, arguments);
},
/** when the model is fetched, merge this.object into this */
parse: function (response, options) {
var attributes = response;
attributes = this._mergeObject(attributes);
return attributes;
},
};
/** @class Concrete class of Generic DatasetCollectionElement */
var DatasetCollectionElement = Backbone.Model.extend(BASE_MVC.LoggableMixin)
.extend(DatasetCollectionElementMixin)
.extend({ _logNamespace: "collections" });
//==============================================================================
/** @class Base/Abstract Backbone collection for Generic DCEs -
current may be associated with a dataset (DatasetDCECollection)
or another collection (NestedDCDCECollection).
*/
var DCECollection = Backbone.Collection.extend(BASE_MVC.LoggableMixin).extend(
/** @lends DCECollection.prototype */ {
_logNamespace: "collections",
model: DatasetCollectionElement,
/** String representation. */
toString: function () {
return ["DatasetCollectionElementCollection(", this.length, ")"].join("");
},
}
);
//==============================================================================
/** @class Backbone model for a dataset collection element that is a dataset (HDA).
*/
var DatasetDCE = DATASET_MODEL.DatasetAssociation.extend(
BASE_MVC.mixin(
DatasetCollectionElementMixin,
/** @lends DatasetDCE.prototype */ {
/** url fn */
url: function () {
// won't always be an hda
if (!this.has("history_id")) {
console.warn("no endpoint for non-hdas within a collection yet");
// (a little silly since this api endpoint *also* points at hdas)
return `${getAppRoot()}api/datasets`;
}
const datasetId = this._getDatasetId();
return `${getAppRoot()}api/histories/${this.get("history_id")}/contents/${datasetId}`;
},
_getDatasetId: function () {
// I'm a DCE acting as dataset, this URL needs to be the dataset URL so
// use element_id instead of id. See note above in _mergeObject and
// discussion on #3782 for more context.
return this.get("element_id");
},
defaults: _.extend(
{},
DATASET_MODEL.DatasetAssociation.prototype.defaults,
DatasetCollectionElementMixin.defaults
),
_downloadQueryParameters: function () {
// Setting the file extension to just 'data' defers that
// decision to the serverside, setting based on the datatype.
var fileExt = this.get("file_ext") || "data";
var elementIdentifier = this.get("element_identifier");
var parentHdcaId = this.get("parent_hdca_id");
return `?to_ext=${fileExt}&hdca_id=${parentHdcaId}&element_identifier=${elementIdentifier}`;
},
// because all objects have constructors (as this hashmap would even if this next line wasn't present)
// the constructor in hcontentMixin won't be attached by BASE_MVC.mixin to this model
// - re-apply manually for now
/** call the mixin constructor */
constructor: function (attributes, options) {
this.debug("\t DatasetDCE.constructor:", attributes, options);
//DATASET_MODEL.DatasetAssociation.prototype.constructor.call( this, attributes, options );
DatasetCollectionElementMixin.constructor.call(this, attributes, options);
},
/** Does this model already contain detailed data (as opposed to just summary level data)? */
hasDetails: function () {
return this.elements && this.elements.length;
},
/** String representation. */
toString: function () {
var objStr = this.get("element_identifier");
return `DatasetDCE(${objStr})`;
},
}
)
);
//==============================================================================
/** @class DCECollection of DatasetDCE's (a list of datasets, a pair of datasets).
*/
var DatasetDCECollection = DCECollection.extend(
/** @lends DatasetDCECollection.prototype */ {
model: DatasetDCE,
/** String representation. */
toString: function () {
return ["DatasetDCECollection(", this.length, ")"].join("");
},
}
);
//_________________________________________________________________________________________________ COLLECTIONS
/** @class Backbone model for Dataset Collections.
* The DC API returns an array of JSON objects under the attribute elements.
* This model:
* - removes that array/attribute ('elements') from the model,
* - creates a bbone collection (of the class defined in the 'collectionClass' attribute),
* - passes that json onto the bbone collection
* - caches the bbone collection in this.elements
*/
var DatasetCollection = Backbone.Model.extend(BASE_MVC.LoggableMixin)
.extend(BASE_MVC.SearchableModelMixin)
.extend(
/** @lends DatasetCollection.prototype */ {
_logNamespace: "collections",
/** default attributes for a model */
defaults: {
/* 'list', 'paired', or 'list:paired' */
collection_type: null,
//??
deleted: false,
},
/** Which class to use for elements */
collectionClass: function () {
if (this.attributes.collection_type.indexOf(":") > 0) {
return NestedDCDCECollection;
} else {
return DatasetDCECollection;
}
},
/** set up: create elements instance var and (on changes to elements) update them */
initialize: function (model, options) {
this.debug(`${this}(DatasetCollection).initialize:`, model, options, this);
this.elements = this._createElementsModel();
this.on("change:elements", function () {
this.log("change:elements");
//TODO: prob. better to update the collection instead of re-creating it
this.elements = this._createElementsModel();
});
},
/** move elements model attribute to full collection */
_createElementsModel: function () {
var collectionClass = this.collectionClass();
this.debug(`${this}._createElementsModel`, collectionClass, this.get("elements"), this.elements);
//TODO: same patterns as DatasetCollectionElement _createObjectModel - refactor to BASE_MVC.hasSubModel?
var elements = this.get("elements") || [];
this.unset("elements", { silent: true });
var parentHdcaId = this.get("parent_hdca_id") || this.get("id");
_.each(elements, (element, index) => {
_.extend(element, {
parent_hdca_id: parentHdcaId,
});
// Warning: MEGA-hack ahead...
if (!element.element_type && !element.object) {
// The DCE has to be in error state... so we simulate it
_.extend(element, {
element_type: "hda",
object: { state: "error" },
});
}
});
this.elements = new collectionClass(elements);
//this.debug( 'collectionClass:', this.collectionClass + '', this.elements );
return this.elements;
},
// ........................................................................ common queries
/** pass the elements back within the model json when this is serialized */
toJSON: function () {
var json = Backbone.Model.prototype.toJSON.call(this);
if (this.elements) {
json.elements = this.elements.toJSON();
}
return json;
},
/** Is this collection in a 'ready' state no processing (for the collection) is left
* to do on the server.
*/
inReadyState: function () {
var populated = this.get("populated");
return this.isDeletedOrPurged() || populated;
},
//TODO:?? the following are the same interface as DatasetAssociation - can we combine?
/** Does the DC contain any elements yet? Is a fetch() required? */
hasDetails: function () {
return this.elements.length !== 0;
},
/** Given the filters, what models in this.elements would be returned? */
getVisibleContents: function (filters) {
// filters unused for now
return this.elements;
},
// ........................................................................ ajax
/** override to use actual Dates objects for create/update times */
parse: function (response, options) {
var parsed = Backbone.Model.prototype.parse.call(this, response, options);
if (parsed.create_time) {
parsed.create_time = new Date(parsed.create_time);
}
if (parsed.update_time) {
parsed.update_time = new Date(parsed.update_time);
}
return parsed;
},
/** save this collection, _Mark_ing it as deleted (just a flag) */
delete: function (recursive, purge, options) {
recursive = recursive || false;
purge = purge || false;
if (this.get("deleted")) {
return $.when();
}
options = Utils.merge(options, { method: "delete" });
return this.save({ deleted: true, recursive: recursive, purge: purge }, options);
},
/** save this collection, _Mark_ing it as undeleted */
undelete: function (options) {
if (!this.get("deleted")) {
return $.when();
}
return this.save({ deleted: false }, options);
},
/** Is this collection deleted or purged? */
isDeletedOrPurged: function () {
return this.get("deleted") || this.get("purged");
},
// ........................................................................ searchable
/** searchable attributes for collections */
searchAttributes: ["name", "tags"],
// ........................................................................ misc
/** String representation */
toString: function () {
var idAndName = [this.get("id"), this.get("name") || this.get("element_identifier")];
return `DatasetCollection(${idAndName.join(",")})`;
},
}
);
//_________________________________________________________________________________________________ NESTED COLLECTIONS
// this is where things get weird, man. Weird.
//TODO: it might be possible to compact all the following...I think.
//==============================================================================
/** @class Backbone model for a Generic DatasetCollectionElement that is also a DatasetCollection
* (a nested collection).
*/
var NestedDCDCE = DatasetCollection.extend(
BASE_MVC.mixin(
DatasetCollectionElementMixin,
/** @lends NestedDCDCE.prototype */ {
// because all objects have constructors (as this hashmap would even if this next line wasn't present)
// the constructor in hcontentMixin won't be attached by BASE_MVC.mixin to this model
// - re-apply manually it now
/** call the mixin constructor */
constructor: function (attributes, options) {
this.debug("\t NestedDCDCE.constructor:", attributes, options);
DatasetCollectionElementMixin.constructor.call(this, attributes, options);
},
/** String representation. */
toString: function () {
var objStr = this.object ? `${this.object}` : this.get("element_identifier");
return ["NestedDCDCE(", objStr, ")"].join("");
},
}
)
);
//==============================================================================
/** @class Backbone collection containing Generic NestedDCDCE's (nested dataset collections).
*/
var NestedDCDCECollection = DCECollection.extend(
/** @lends NestedDCDCECollection.prototype */ {
/** This is a collection of nested collections */
model: NestedDCDCE,
/** String representation. */
toString: function () {
return ["NestedDCDCECollection(", this.length, ")"].join("");
},
}
);
//==============================================================================
export default {
DatasetCollection: DatasetCollection,
};
@@ -1,114 +0,0 @@
import $ from "jquery";
import { getGalaxyInstance } from "app";
import DC_VIEW from "mvc/collection/collection-view";
import DC_EDIT from "mvc/collection/collection-li-edit";
import { mountModelTags } from "components/Tags";
import _l from "utils/localization";
import "ui/editable-text";
/* =============================================================================
TODO:
============================================================================= */
/** @class editable View/Controller for a dataset collection.
*/
var _super = DC_VIEW.CollectionView;
var CollectionViewEdit = _super.extend(
/** @lends CollectionView.prototype */ {
//MODEL is either a DatasetCollection (or subclass) or a DatasetCollectionElement (list of pairs)
/** logger used to record this.log messages, commonly set to console */
//logger : console,
/** sub view class used for datasets */
DatasetDCEViewClass: DC_EDIT.DatasetDCEListItemEdit,
/** sub view class used for nested collections */
NestedDCDCEViewClass: DC_EDIT.NestedDCDCEListItemEdit,
getNestedDCDCEViewClass: function () {
return DC_EDIT.NestedDCDCEListItemEdit.extend({
foldoutPanelClass: CollectionViewEdit,
});
},
// ......................................................................... SET UP
/** Set up the view, set up storage, bind listeners to HistoryContents events
* @param {Object} attributes optional settings for the panel
*/
initialize: function (attributes) {
_super.prototype.initialize.call(this, attributes);
},
/** In this override, make the collection name editable
*/
_setUpBehaviors: function ($where) {
$where = $where || this.$el;
_super.prototype._setUpBehaviors.call(this, $where);
if (!this.model) {
return;
}
// anon users shouldn't have access to any of the following
const Galaxy = getGalaxyInstance();
if (!Galaxy.user || Galaxy.user.isAnonymous()) {
return;
}
this.tagsEditorShown = true;
//TODO: extract
var panel = this;
var nameSelector = "> .controls .name";
$where
.find(nameSelector)
.attr("title", _l("Click to rename collection"))
.tooltip({ placement: "bottom" })
.make_text_editable({
on_finish: function (newName) {
var previousName = panel.model.get("name");
if (newName && newName !== previousName) {
panel.$el.find(nameSelector).text(newName);
panel.model.save({ name: newName }).fail(() => {
panel.$el.find(nameSelector).text(panel.model.previous("name"));
});
} else {
panel.$el.find(nameSelector).text(previousName);
}
},
});
const el = $where.find(".tags-display")[0];
const propsData = {
model: this.model,
disabled: false,
context: "collection-view-edit",
};
const vm = mountModelTags(propsData, el);
const toggleEditor = () => {
$(vm.$el).toggleClass("active");
this.tagsEditorShown = $(vm.$el).hasClass("active");
};
if (this.tagsEditorShown) {
const editorIsOpen = $(vm.$el).hasClass("active");
if (!editorIsOpen) {
toggleEditor();
}
}
},
// ........................................................................ misc
/** string rep */
toString: function () {
return `CollectionViewEdit(${this.model ? this.model.get("name") : ""})`;
},
}
);
//==============================================================================
export default {
CollectionViewEdit: CollectionViewEdit,
};
@@ -1,235 +0,0 @@
import _ from "underscore";
import { getAppRoot } from "onload/loadConfig";
import LIST_VIEW from "mvc/list/list-view";
import DC_LI from "mvc/collection/collection-li";
import _l from "utils/localization";
var logNamespace = "collections";
/* =============================================================================
TODO:
============================================================================= */
/** @class non-editable, read-only View/Controller for a dataset collection.
*/
var _super = LIST_VIEW.ModelListPanel;
var CollectionView = _super.extend(
/** @lends CollectionView.prototype */ {
//MODEL is either a DatasetCollection (or subclass) or a DatasetCollectionElement (list of pairs)
_logNamespace: logNamespace,
className: `${_super.prototype.className} dataset-collection-panel`,
/** sub view class used for datasets */
DatasetDCEViewClass: DC_LI.DatasetDCEListItemView,
/** key of attribute in model to assign to this.collection */
modelCollectionKey: "elements",
// ......................................................................... SET UP
/** Set up the view, set up storage, bind listeners to HistoryContents events
* @param {Object} attributes optional settings for the panel
*/
initialize: function (attributes) {
_super.prototype.initialize.call(this, attributes);
this.linkTarget = attributes.linkTarget || "_blank";
this.dragItems = true;
this.hasUser = attributes.hasUser;
/** A stack of panels that currently cover or hide this panel */
this.panelStack = [];
/** The text of the link to go back to the panel containing this one */
this.parentName = attributes.parentName;
/** foldout or drilldown */
this.foldoutStyle = attributes.foldoutStyle || "foldout";
this.downloadUrl = `${getAppRoot()}api/dataset_collections/${this.model.attributes.id}/download`;
},
getNestedDCDCEViewClass: function () {
return DC_LI.NestedDCDCEListItemView.extend({
foldoutPanelClass: CollectionView,
});
},
_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
// this causes display: block for those elems after the use of show/hide animations
// animations are removed from this view for now until fixed
panel._swapNewRender($newRender);
panel.trigger("rendered", panel);
},
handleWarning: function ($newRender) {
var $warns = $newRender.find(".elements-warning");
if (this.model.get("populated_state") === "failed") {
var error = _l(`${this.model.get("populated_state_message")}`);
$warns.html(`<div class="errormessagesmall">${error}</div>`);
return;
}
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`);
$warns.html(`<div class="warningmessagesmall">${warning}</div>`);
}
},
// ------------------------------------------------------------------------ sub-views
/** In this override, use model.getVisibleContents */
_filterCollection: function () {
//TODO: should *not* be model.getVisibleContents - visibility is not model related
return this.model.getVisibleContents();
},
/** override to return proper view class based on element_type */
_getItemViewClass: function (model) {
//this.debug( this + '._getItemViewClass:', model );
//TODO: subclasses use DCEViewClass - but are currently unused - decide
switch (model.get("element_type")) {
case "hda":
return this.DatasetDCEViewClass;
case "dataset_collection":
return this.getNestedDCDCEViewClass();
}
throw new TypeError("Unknown element type:", model.get("element_type"));
},
/** override to add link target and anon */
_getItemViewOptions: function (model) {
var options = _super.prototype._getItemViewOptions.call(this, model);
return _.extend(options, {
linkTarget: this.linkTarget,
hasUser: this.hasUser,
//TODO: could move to only nested: list:paired
foldoutStyle: this.foldoutStyle,
});
},
// ------------------------------------------------------------------------ collection sub-views
/** In this override, add/remove expanded/collapsed model ids to/from web storage */
_setUpItemViewListeners: function (view) {
var panel = this;
_super.prototype._setUpItemViewListeners.call(panel, view);
// use pub-sub to: handle drilldown expansion and collapse
panel.listenTo(view, {
"expanded:drilldown": function (v, drilldown) {
this._expandDrilldownPanel(drilldown);
},
"collapsed:drilldown": function (v, drilldown) {
this._collapseDrilldownPanel(drilldown);
},
});
return this;
},
/** Handle drill down by hiding this panels list and controls and showing the sub-panel */
_expandDrilldownPanel: function (drilldown) {
this.panelStack.push(drilldown);
// hide this panel's controls and list, set the name for back navigation, and attach to the $el
this.$("> .controls").add(this.$list()).hide();
drilldown.parentName = this.model.get("name");
this.$el.append(drilldown.render().$el);
},
/** Handle drilldown close by freeing the panel and re-rendering this panel */
_collapseDrilldownPanel: function (drilldown) {
this.panelStack.pop();
this.render();
},
// ------------------------------------------------------------------------ panel events
/** event map */
events: {
"click .navigation .back": "close",
},
/** close/remove this collection panel */
close: function (event) {
this.remove();
this.trigger("close");
},
// ........................................................................ misc
/** string rep */
toString: function () {
return `CollectionView(${this.model ? this.model.get("name") : ""})`;
},
}
);
//------------------------------------------------------------------------------ TEMPLATES
CollectionView.prototype.templates = (() => {
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,
};
-434
View File
@@ -1,434 +0,0 @@
import $ from "jquery";
import _ from "underscore";
import Backbone from "backbone";
import DATASET from "mvc/dataset/dataset-model";
import DATASET_LIST from "mvc/dataset/dataset-list";
import MODAL from "mvc/ui/ui-modal";
import BASE_MVC from "mvc/base-mvc";
import _l from "utils/localization";
var logNamespace = "dataset";
/* ============================================================================
TODO:
does this really work with mixed contents?
Single dataset choice: allow none?
tooltips rendered *behind* modal
collection selector
better handling when no results returned from filterDatasetJSON
pass optional subtitle from choice display to modal
onfirstclick
drop target
return modal with promise?
auto showing the modal may not be best
============================================================================ */
/** Filters an array of dataset plain JSON objs.
*/
function _filterDatasetJSON(datasetJSON, where, datasetsOnly) {
//TODO: replace with _.matches (underscore 1.6.0)
function matches(obj, toMatch) {
for (var key in toMatch) {
if (Object.prototype.hasOwnProperty.call(toMatch, key)) {
if (obj[key] !== toMatch[key]) {
return false;
}
}
}
return true;
}
return datasetJSON.filter((json) => {
console.debug(json);
return (
!json.deleted &&
json.visible &&
(!datasetsOnly || json.collection_type === undefined) &&
matches(json, where)
);
});
}
// ============================================================================
/** Given an array of plain JSON objs rep. datasets, show a modal allowing a choice
* of one or more of those datasets.
*
* Pass:
* an array of plain JSON objects representing allowed dataset choices
* a map of options (see below)
*
* Options:
* datasetsOnly: T: display only datasets, F: datasets + dataset collections
* where: a map of attributes available choices *must have* (defaults to { state: 'ok' })
* multiselect: T: user can select more than one, F: only one
* selected: array of dataset ids to make them selected by default
*
* @example:
* var datasetJSON = // from ajax or bootstrap
* // returns a jQuery promise (that 'fail's only if no datasets are found matching 'where' below)
* var choice = new DatasetChoiceModal( datasetJSON, {
* datasetsOnly : false,
* where : { state: 'ok', file_ext: 'bed', ... },
* multiselect : true,
* selected : [ 'df7a1f0c02a5b08e', 'abcdef0123456789' ]
*
* }).done( function( json ){
* if( json ){
* console.debug( json );
* // returned choice will always be an array (single or multi)
* // [{ <selected dataset JSON 1>, <selected dataset JSON 2>, ... }]
* // ... do stuff
* } else {
* // json will === null if the user cancelled selection
* console.debug( 'cancelled' );
* }
* });
*/
var DatasetChoiceModal = (datasetJSON, options) => {
// option defaults
options = _.defaults(options || {}, {
// show datasets or datasets and collections
datasetsOnly: true,
// map of attributes to filter datasetJSON by
where: { state: "ok" },
// select more than one dataset?
multiselect: false,
// any dataset ids that will display as already selected
selected: [],
});
// default title should depend on multiselect
options.title = options.title || (options.multiselect ? _l("Choose datasets:") : _l("Choose a dataset:"));
var modal;
var list;
var buttons;
var promise = $.Deferred();
var filterFn = options.filter || _filterDatasetJSON;
// filter the given datasets and if none left return a rejected promise for use with fail()
datasetJSON = filterFn(datasetJSON, options.where, options.datasetsOnly);
if (!datasetJSON.length) {
return promise.reject("No matches found");
}
// resolve the returned promise with the json of the selected datasets
function resolveWithSelected() {
promise.resolve(list.getSelectedModels().map((model) => model.toJSON()));
}
// if multiselect - add a button for the user to complete the changes
if (options.multiselect) {
buttons = {};
buttons[_l("Ok")] = resolveWithSelected;
}
// create a full-height modal that's cancellable, remove unneeded elements and styles
modal = new MODAL.View({
height: "auto",
buttons: buttons,
closing_events: true,
closing_callback: function () {
promise.resolve(null);
},
body: ['<div class="list-panel"></div>'].join(""),
});
modal.$(".modal-header").remove();
modal.$(".modal-footer").css("margin-top", "0px");
// attach a dataset list (of the filtered datasets) to that modal that's selectable
list = new DATASET_LIST.DatasetList({
title: options.title,
subtitle:
options.subtitle ||
_l(
[
//TODO: as option
"Click the checkboxes on the right to select datasets. ",
"Click the datasets names to see their details. ",
].join("")
),
el: modal.$body.find(".list-panel"),
selecting: true,
selected: options.selected,
collection: new DATASET.DatasetAssociationCollection(datasetJSON),
});
// when the list is rendered, show the modal (also add a specifying class for css)
list.once("rendered:initial", () => {
modal.show();
modal.$el.addClass("dataset-choice-modal");
});
if (!options.multiselect) {
// if single select, remove the all/none list actions from the panel
list.on("rendered", () => {
list.$(".list-actions").hide();
});
// if single select, immediately resolve on a single selection
list.on("view:selected", (view) => {
promise.resolve([view.model.toJSON()]);
});
}
list.render(0);
// return the promise, and on any resolution close the modal
return promise.always(() => {
modal.hide();
});
};
// ============================================================================
/** Activator for single dataset selection modal and display of the selected dataset.
* The activator/display will show as a single div and, when a dataset is selected,
* show the name and details of the selected dataset.
*
* When clicked the div will generate a DatasetChoiceModal of the available choices.
*
* Options:
* datasetJSON: array of plain json objects representing allowed choices
* datasetsOnly: T: only show datasets in the allowed choices, F: datasets + collections
* where: map of attributes to filter datasetJSON by (e.g. { file_ext: 'bed' })
* label: the label/prompt displayed
* selected: array of dataset ids that will show as already selected in the control
*
* @example:
* var choice1 = new DATASET_CHOICE.DatasetChoice({
* datasetJSON : datasetJSON,
* label : 'Input dataset',
* selected : [ 'df7a1f0c02a5b08e' ]
* });
* $( 'body' ).append( choice1.render().$el )
*
* Listen to the DatasetChoice to react to changes in the user's choice/selection:
* @example:
* choice1.on( 'selected', function( chooser, selectionJSONArray ){
* // ... do stuff with new selections
* });
*/
var DatasetChoice = Backbone.View.extend(BASE_MVC.LoggableMixin).extend({
_logNamespace: logNamespace,
className: "dataset-choice",
/** set up defaults, options, and listeners */
initialize: function (attributes) {
this.debug(`${this}(DatasetChoice).initialize:`, attributes);
this.label = attributes.label !== undefined ? _l(attributes.label) : "";
this.where = attributes.where;
this.datasetsOnly = attributes.datasetsOnly !== undefined ? attributes.datasetsOnly : true;
this.datasetJSON = attributes.datasetJSON || [];
this.selected = attributes.selected || [];
this._setUpListeners();
},
/** add any (bbone) listeners */
_setUpListeners: function () {
//this.on( 'all', function(){
// this.log( this + '', arguments );
//});
},
/** render the view */
render: function () {
var json = this.toJSON();
this.$el.html(this._template(json));
this.$(".selected").replaceWith(this._renderSelected(json));
return this;
},
/** return plain html for the overall control */
_template: function (json) {
return _.template(
["<label>", '<span class="prompt"><%- label %></span>', '<div class="selected"></div>', "</label>"].join("")
)(json);
},
/** return jQ DOM for the selected dataset (only one) */
_renderSelected: function (json) {
if (json.selected.length) {
//TODO: break out?
return $(
_.template(
[
'<div class="selected">',
'<span class="title"><%- selected.hid %>: <%- selected.name %></span>',
'<span class="subtitle">',
"<i><%- selected.misc_blurb %></i>",
"<i>",
`${_l("format")}: `,
"<%- selected.file_ext %></i>",
"<i><%- selected.misc_info %></i>",
"</span>",
"</div>",
].join(""),
{ variable: "selected" }
)(json.selected[0])
);
}
return $(['<span class="none-selected-msg">(', _l("click to select a dataset"), ")</span>"].join(""));
},
//TODO:?? why not just pass in view?
/** return a plain JSON object with both the view and dataset attributes */
toJSON: function () {
var chooser = this;
return {
label: chooser.label,
datasets: chooser.datasetJSON,
selected: _.compact(
_.map(chooser.selected, (id) =>
_.findWhere(chooser.datasetJSON, {
id: id,
})
)
),
};
},
/** event map: when to open the modal */
events: {
// the whole thing functions as a button
click: "chooseWithModal",
},
//TODO:?? modal to prop of this?
//TODO:?? should be able to handle 'none selectable' on initialize
/** open the modal and handle the promise representing the user's choice
* @fires 'selected' when the user selects dataset(s) - passed full json of the selected datasets
* @fires 'cancelled' when the user clicks away/closes the modal (no selection made) - passed this
* @fires 'error' if the modal has no selectable datasets based on this.where - passed this and other args
*/
chooseWithModal: function () {
var chooser = this;
return this._createModal()
.done((json) => {
if (json) {
chooser.selected = _.pluck(json, "id");
chooser.trigger("selected", chooser, json);
chooser.render();
} else {
chooser.trigger("cancelled", chooser);
}
})
.fail(function () {
chooser.trigger("error", chooser, arguments);
});
},
/** create and return the modal to use for choosing */
_createModal: function () {
return new DatasetChoiceModal(this.datasetJSON, this._getModalOptions());
},
/** return a plain JSON containing the options to pass to the modal */
_getModalOptions: function () {
return {
title: this.label,
multiselect: false,
selected: this.selected,
where: this.where,
datasetsOnly: this.datasetsOnly,
};
},
// ------------------------------------------------------------------------ misc
/** string rep */
toString: function () {
return `DatasetChoice(${this.selected})`;
},
});
// ============================================================================
/** Activator for multiple dataset selection modal and display of the selected datasets.
* The activator/display will show as a table of all choices.
*
* See DatasetChoice (above) for example usage.
*
* Additional options:
* showHeaders: T: show headers for selected dataset attributes in the display table
* cells: map of attribute keys -> Human readable/localized column headers
* (e.g. { file_ext: _l( 'Format' ) }) - defaults are listed below
*/
var MultiDatasetChoice = DatasetChoice.extend({
className: `${DatasetChoice.prototype.className} multi`,
/** default (dataset attribute key -> table header text) map of what cells to display in the table */
cells: {
hid: _l("History #"),
name: _l("Name"),
misc_blurb: _l("Summary"),
file_ext: _l("Format"),
genome_build: _l("Genome"),
tags: _l("Tags"),
annotation: _l("Annotation"),
},
/** in this override, add the showHeaders and cells options */
initialize: function (attributes) {
this.showHeaders = attributes.showHeaders !== undefined ? attributes.showHeaders : true;
this.cells = attributes.cells || this.cells;
DatasetChoice.prototype.initialize.call(this, attributes);
},
/** in this override, display the selected datasets as a table with optional headers */
_renderSelected: function (json) {
if (json.selected.length) {
return $(
_.template(
[
'<table class="selected">',
"<% if( json.showHeaders ){ %>",
"<thead><tr>",
"<% _.map( json.cells, function( val, key ){ %>",
"<th><%- val %></th>",
"<% }); %>",
"</tr></thead>",
"<% } %>",
"<tbody>",
"<% _.map( json.selected, function( selected ){ %>",
"<tr>",
"<% _.map( json.cells, function( val, key ){ %>",
'<td class="cell-<%- key %>"><%- selected[ key ] %></td>',
"<% }) %>",
"</tr>",
"<% }); %>",
"</tbody>",
"</table>",
].join(""),
{ variable: "json" }
)(json)
);
}
return $(['<span class="none-selected-msg">(', _l("click to select a dataset"), ")</span>"].join(""));
},
/** in this override, send the showHeaders and cells options as well */
toJSON: function () {
return _.extend(DatasetChoice.prototype.toJSON.call(this), {
showHeaders: this.showHeaders,
cells: this.cells,
});
},
/** in this override, set multiselect to true */
_getModalOptions: function () {
return _.extend(DatasetChoice.prototype._getModalOptions.call(this), {
multiselect: true,
});
},
// ------------------------------------------------------------------------ misc
/** string rep */
toString: function () {
return `DatasetChoice(${this.selected})`;
},
});
// ============================================================================
export default {
DatasetChoiceModal: DatasetChoiceModal,
DatasetChoice: DatasetChoice,
MultiDatasetChoice: MultiDatasetChoice,
};
-472
View File
@@ -1,472 +0,0 @@
import _ from "underscore";
import $ from "jquery";
import { getAppRoot } from "onload/loadConfig";
import { getGalaxyInstance } from "app";
import STATES from "mvc/dataset/states";
import DATASET_LI from "mvc/dataset/dataset-li";
import { mountModelTags } from "components/Tags";
import ANNOTATIONS from "mvc/annotation";
import faIconButton from "ui/fa-icon-button";
import BASE_MVC from "mvc/base-mvc";
import _l from "utils/localization";
//==============================================================================
var _super = DATASET_LI.DatasetListItemView;
/** @class Editing view for DatasetAssociation.
*/
var DatasetListItemEdit = _super.extend(
/** @lends DatasetListItemEdit.prototype */ {
/** set up: options */
initialize: function (attributes) {
_super.prototype.initialize.call(this, attributes);
this.hasUser = attributes.hasUser;
/** allow user purge of dataset files? */
this.purgeAllowed = attributes.purgeAllowed || false;
//TODO: move to HiddenUntilActivatedViewMixin
/** should the tags editor be shown or hidden initially? */
this.tagsEditorShown = attributes.tagsEditorShown || false;
/** should the tags editor be shown or hidden initially? */
this.annotationEditorShown = attributes.annotationEditorShown || false;
},
// ......................................................................... titlebar actions
/** In this override, add the other two primary actions: edit and delete */
_renderPrimaryActions: function () {
var actions = _super.prototype._renderPrimaryActions.call(this);
if (this.model.get("state") === STATES.NOT_VIEWABLE) {
return actions;
}
// render the display, edit attr and delete icon-buttons
return _super.prototype._renderPrimaryActions
.call(this)
.concat([this._renderEditButton(), this._renderDeleteButton()]);
},
//TODO: move titleButtons into state renderers, remove state checks in the buttons
/** Render icon-button to edit the attributes (format, permissions, etc.) this dataset. */
_renderEditButton: function () {
var self = this;
// don't show edit while uploading, in-accessible
// DO show if in error (ala previous history panel)
if (this.model.get("state") === STATES.DISCARDED || !this.model.get("accessible")) {
return null;
}
var purged = this.model.get("purged");
var deleted = this.model.get("deleted");
var editBtnData = {
title: _l("Edit attributes"),
href: `${getAppRoot()}datasets/edit?dataset_id=${this.model.get("element_id") || this.model.get("id")}`,
faIcon: "fa-pencil",
classes: "edit-btn",
onclick: function (ev) {
if (!deleted && !purged) {
const Galaxy = getGalaxyInstance();
if (Galaxy.router) {
ev.preventDefault();
const identifier = self.model.get("element_id") || self.model.get("id");
Galaxy.router.push("datasets/edit", {
dataset_id: identifier,
});
}
}
},
};
// disable if purged or deleted and explain why in the tooltip
if (deleted || purged) {
editBtnData.disabled = true;
if (purged) {
editBtnData.title = _l("Cannot edit attributes of datasets removed from disk");
} else if (deleted) {
editBtnData.title = _l("Undelete dataset to edit attributes");
}
// disable if still uploading or new
} else if ([STATES.UPLOAD, STATES.NEW].includes(this.model.get("state"))) {
editBtnData.disabled = true;
editBtnData.title = _l("This dataset is not yet editable");
}
return faIconButton(editBtnData);
},
/** Render icon-button to delete this hda. */
_renderDeleteButton: function () {
// don't show delete if...
if (!this.model.get("accessible")) {
return null;
}
var self = this;
var deletedAlready = this.model.isDeletedOrPurged();
return faIconButton({
title: !deletedAlready ? _l("Delete") : _l("Dataset is already deleted"),
disabled: deletedAlready,
faIcon: "fa-times",
classes: "delete-btn",
onclick: function () {
self.$el.find(".icon-btn.delete-btn").tooltip("dispose");
self.model["delete"]();
},
});
},
// ......................................................................... details
/** In this override, add tags and annotations controls, make the ? dbkey a link to editing page */
_renderDetails: function () {
//TODO: generalize to be allow different details for each state
var $details = _super.prototype._renderDetails.call(this);
var state = this.model.get("state");
if (!this.model.isDeletedOrPurged()) {
//Enable tagging+annotation regardless of job state (see issue #6330)
this._renderTags($details);
this._renderAnnotation($details);
if ([STATES.OK, STATES.FAILED_METADATA].includes(state)) {
this._makeDbkeyEditLink($details);
}
}
this._setUpBehaviors($details);
return $details;
},
/**************************************************************************
* Render help button to show tool help text without rerunning the tool.
* Issue #2100
*/
_renderToolHelpButton: function () {
var datasetID = this.model.attributes.dataset_id;
var jobID = this.model.attributes.creating_job;
var self = this;
var parseToolBuild = (data) => {
var helpString = `<div id="thdiv-${datasetID}" class="toolhelp">`;
if (data.name && data.help) {
helpString += `<strong>Tool help for ${data.name}</strong><hr/>`;
helpString += data.help;
} else {
helpString += "<strong>Tool help is unavailable for this dataset.</strong><hr/>";
}
helpString += "</div>";
self.$el.find(".details").append($.parseHTML(helpString));
};
var parseToolID = (data) => {
$.ajax({
url: `${getAppRoot()}api/tools/${data.tool_id}/build`,
})
.done((data) => {
parseToolBuild(data);
})
.fail(() => {
parseToolBuild({});
});
};
const Galaxy = getGalaxyInstance();
if (Galaxy.user.id === null) {
return null;
}
return faIconButton({
title: _l("Tool Help"),
classes: "icon-btn",
href: "#",
faIcon: "fa-question",
onclick: function (ev) {
ev.preventDefault();
if (self.$el.find(".toolhelp").length > 0) {
self.$el.find(".toolhelp").toggle();
} else {
$.ajax({
url: `${getAppRoot()}api/jobs/${jobID}`,
})
.done((data) => {
parseToolID(data);
})
.fail(() => {
console.log(
`Failed at recovering job information from the Galaxy API for job id "${jobID}".`
);
});
}
},
});
},
//*************************************************************************
/** Add less commonly used actions in the details section based on state */
_renderSecondaryActions: function () {
var actions = _super.prototype._renderSecondaryActions.call(this);
switch (this.model.get("state")) {
case STATES.UPLOAD:
case STATES.NOT_VIEWABLE:
return actions;
case STATES.ERROR:
// error button comes first
actions.unshift(this._renderErrButton());
return actions.concat([this._renderRerunButton(), this._renderToolHelpButton()]);
case STATES.OK:
case STATES.FAILED_METADATA:
return actions.concat([
this._renderRerunButton(),
this._renderVisualizationsButton(),
this._renderToolHelpButton(),
]);
}
return actions.concat([this._renderRerunButton(), this._renderToolHelpButton()]);
},
/** Render icon-button to report an error on this dataset to the galaxy admin. */
_renderErrButton: function () {
var self = this;
return faIconButton({
title: _l("View or report this error"),
href: `${getAppRoot()}datasets/error?dataset_id=${
self.model.get("element_id") || self.model.get("id")
}`,
classes: "report-error-btn",
faIcon: "fa-bug",
onclick: function (ev) {
const Galaxy = getGalaxyInstance();
if (Galaxy.router) {
ev.preventDefault();
Galaxy.router.push("datasets/error", {
dataset_id: self.model.get("element_id") || self.model.get("id"),
});
}
},
});
},
/** Render icon-button to re-run the job that created this dataset. */
_renderRerunButton: function () {
var creating_job = this.model.get("creating_job");
if (this.model.get("rerunnable")) {
return faIconButton({
title: _l("Run this job again"),
href: this.model.urls.rerun,
classes: "rerun-btn",
target: this.linkTarget,
faIcon: "fa-redo",
onclick: function (ev) {
const Galaxy = getGalaxyInstance();
if (Galaxy.router) {
ev.preventDefault();
Galaxy.router.push("/", {
job_id: creating_job,
});
}
},
});
}
},
/** Render an icon-button or popupmenu of links based on the applicable visualizations */
_renderVisualizationsButton: function () {
const Galaxy = getGalaxyInstance();
const dsid = this.model.get("element_id") || this.model.get("id");
const url = getAppRoot() + "visualizations?dataset_id=" + dsid;
return faIconButton({
title: _l("Visualize this data"),
href: url,
classes: "visualization-link",
faIcon: "fa-bar-chart-o",
onclick: (ev) => {
if (Galaxy.frame && Galaxy.frame.active) {
ev.preventDefault();
Galaxy.frame.add({ url: url, title: "Visualization" });
} else if (Galaxy.router) {
ev.preventDefault();
Galaxy.router.push("visualizations", {
dataset_id: dsid,
});
Galaxy.trigger("activate-hda", dsid);
}
},
});
},
/** Render the tags list/control */
_renderTags: function ($where) {
if (!this.hasUser) {
return;
}
const el = $where.find(".tags-display")[0];
const propsData = {
model: this.model,
disabled: false,
context: "dataset-li-edit",
};
const vm = mountModelTags(propsData, el);
// tag icon button open/closes
const activator = faIconButton({
title: _l("Edit dataset tags"),
classes: "tag-btn",
faIcon: "fa-tags",
}).appendTo($where.find(".actions .right"));
const toggleEditor = () => {
$(vm.$el).toggleClass("active");
this.tagsEditorShown = $(vm.$el).hasClass("active");
};
activator.on("click", toggleEditor);
if (this.tagsEditorShown) {
const editorIsOpen = $(vm.$el).hasClass("active");
if (!editorIsOpen) {
toggleEditor();
}
}
return vm;
},
/** Render the annotation display/control */
_renderAnnotation: function ($where) {
if (!this.hasUser) {
return;
}
var view = this;
this.annotationEditor = new ANNOTATIONS.AnnotationEditor({
model: this.model,
el: $where.find(".annotation-display"),
onshowFirstTime: function () {
this.render();
},
// persist state on the hda view (and not the editor) since these are currently re-created each time
onshow: function () {
view.annotationEditorShown = true;
},
onhide: function () {
view.annotationEditorShown = false;
},
$activator: faIconButton({
title: _l("Edit dataset annotation"),
classes: "annotate-btn",
faIcon: "fa-comment",
}).appendTo($where.find(".actions .right")),
});
if (this.annotationEditorShown) {
this.annotationEditor.toggle(true);
}
},
/** If the format/dbkey/genome_build isn't set, make the display a link to the edit page */
_makeDbkeyEditLink: function ($details) {
// make the dbkey a link to editing
if (this.model.get("metadata_dbkey") === "?" && !this.model.isDeletedOrPurged()) {
var editableDbkey = $('<a class="value">?</a>')
.attr("href", this.model.urls.edit)
.attr("target", "_top");
$details.find(".dbkey .value").replaceWith(editableDbkey);
}
},
// ......................................................................... events
/** event map */
events: _.extend(_.clone(_super.prototype.events), {
"click .undelete-link": "_clickUndeleteLink",
"click .purge-link": "_clickPurgeLink",
"click .edit-btn": function (ev) {
this.trigger("edit", this, ev);
},
"click .delete-btn": function (ev) {
this.trigger("delete", this, ev);
},
"click .rerun-btn": function (ev) {
this.trigger("rerun", this, ev);
},
"click .report-err-btn": function (ev) {
this.trigger("report-err", this, ev);
},
"click .visualization-btn": function (ev) {
this.trigger("visualize", this, ev);
},
"click .dbkey a": function (ev) {
this.trigger("edit", this, ev);
},
}),
/** listener for item undelete (in the messages section) */
_clickUndeleteLink: function (ev) {
this.model.undelete();
return false;
},
/** listener for item purge (in the messages section) */
_clickPurgeLink: function (ev) {
if (window.confirm(_l("This will permanently remove the data in your dataset. Are you sure?"))) {
this.model.purge();
}
return false;
},
// ......................................................................... misc
/** string rep */
toString: function () {
var modelString = this.model ? `${this.model}` : "(no model)";
return `HDAEditView(${modelString})`;
},
}
);
// ............................................................................ TEMPLATES
/** underscore templates */
DatasetListItemEdit.prototype.templates = (() => {
var warnings = _.extend({}, _super.prototype.templates.warnings, {
failed_metadata: BASE_MVC.wrapTemplate(
[
// in this override, provide a link to the edit page
'<% if( dataset.state === "failed_metadata" ){ %>',
'<div class="failed_metadata-warning warningmessagesmall">',
_l("An error occurred setting the metadata for this dataset"),
'<br /><a href="<%- dataset.urls.edit %>" target="_top">',
_l("Set it manually or retry auto-detection"),
"</a>",
"</div>",
"<% } %>",
],
"dataset"
),
deleted: BASE_MVC.wrapTemplate(
[
// in this override, provide links to undelete or purge the dataset
"<% if( dataset.deleted && !dataset.purged ){ %>",
// deleted not purged
'<div class="deleted-msg warningmessagesmall">',
_l("This dataset has been deleted"),
'<br /><a class="undelete-link" href="javascript:void(0);">',
_l("Undelete it"),
"</a>",
"<% if( view.purgeAllowed ){ %>",
'<br /><a class="purge-link" href="javascript:void(0);">',
_l("Permanently remove it from disk"),
"</a>",
"<% } %>",
"</div>",
"<% } %>",
],
"dataset"
),
});
return _.extend({}, _super.prototype.templates, {
warnings: warnings,
});
})();
//==============================================================================
export default {
DatasetListItemEdit: DatasetListItemEdit,
};
-639
View File
@@ -1,639 +0,0 @@
import _ from "underscore";
import $ from "jquery";
import Backbone from "backbone";
import { getGalaxyInstance } from "app";
import LIST_ITEM from "mvc/list/list-item";
import STATES from "mvc/dataset/states";
import faIconButton from "ui/fa-icon-button";
import BASE_MVC from "mvc/base-mvc";
import _l from "utils/localization";
import { mountNametags } from "components/Nametags";
import { Toast } from "ui/toast";
import { getAppRoot } from "onload/loadConfig";
var logNamespace = "dataset";
/*==============================================================================
TODO:
straighten out state rendering and templates used
inaccessible/STATES.NOT_VIEWABLE is a special case
simplify button rendering
==============================================================================*/
var _super = LIST_ITEM.ListItemView;
/** @class Read only list view for either LDDAs, HDAs, or HDADCEs.
* Roughly, any DatasetInstance (and not a raw Dataset).
*/
export var DatasetListItemView = _super.extend(
/** @lends DatasetListItemView.prototype */ {
_logNamespace: logNamespace,
className: `${_super.prototype.className} dataset`,
//TODO:?? doesn't exactly match an hda's type_id
id: function () {
return ["dataset", this.model.get("id")].join("-");
},
/** 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";
},
// mount new vue component for tags
render: function () {
const result = _super.prototype.render.apply(this, arguments);
this._mountVueNametags();
return result;
},
_mountVueNametags: function () {
const container = this.$(".nametags")[0];
const { id, model_class, tags } = this.model.attributes;
const storeKey = `${model_class}-${id}`;
mountNametags({ storeKey, tags }, container);
},
/** event listeners */
_setUpListeners: function () {
_super.prototype._setUpListeners.call(this);
var self = this;
// re-rendering on any model changes
return self.listenTo(self.model, {
change: function (model) {
// if the model moved into the ready state and is expanded without details, fetch those details now
if (
self.model.changedAttributes().state &&
self.model.inReadyState() &&
self.expanded &&
!self.model.hasDetails()
) {
// normally, will render automatically (due to fetch -> change),
// but! setting_metadata sometimes doesn't cause any other changes besides state
// so, not rendering causes it to seem frozen in setting_metadata state
self.model.fetch({ silent: true }).done(() => {
self.render();
});
} else {
if (_.has(self.model.changed, "tags") && _.keys(self.model.changed).length === 2) {
// If only the tags and update time have changed,
// rerender specifically the titlebar region.
// Otherwise default to the full render.
self._mountVueNametags();
} else {
self.render();
}
}
},
});
},
// ......................................................................... expandable
/** In this override, only get details if in the ready state, get rerunnable if in other states.
* Note: fetch with no 'change' event triggering to prevent automatic rendering.
*/
_fetchModelDetails: function () {
var view = this;
if (view.model.inReadyState() && !view.model.hasDetails()) {
return view.model.fetch({ silent: true });
}
return $.when();
},
// ......................................................................... removal
/** Remove this view's html from the DOM and remove all event listeners.
* @param {Number or String} speed jq effect speed
* @param {Function} callback an optional function called when removal is done (scoped to this view)
*/
remove: function (speed, callback) {
var view = this;
speed = speed || this.fxSpeed;
this.$el.fadeOut(speed, () => {
Backbone.View.prototype.remove.call(view);
if (callback) {
callback.call(view);
}
});
},
// ......................................................................... rendering
/* TODO:
dataset states are the issue primarily making dataset rendering complex
each state should have it's own way of displaying/set of details
often with different actions that can be applied
throw in deleted/purged/visible and things get complicated easily
I've considered (a couple of times) - creating a view for each state
- but recreating the view during an update...seems wrong
*/
/** In this override, add the dataset 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")}`);
this.$el.attr("data-state", this.model.get("state"));
}
return this.$el;
},
// ................................................................................ titlebar
/** In this override, add the dataset display button. */
_renderPrimaryActions: function () {
// render just the display for read-only
return [this._renderDisplayButton()];
},
/** Render icon-button to display dataset data */
_renderDisplayButton: function () {
// don't show display if not viewable or not accessible
var state = this.model.get("state");
if (state === STATES.NOT_VIEWABLE || state === STATES.DISCARDED || !this.model.get("accessible")) {
return null;
}
var displayBtnData = {
target: this.linkTarget,
classes: "display-btn",
};
// show a disabled display if the data's been purged
if (this.model.get("purged")) {
displayBtnData.disabled = true;
displayBtnData.title = _l("Cannot display datasets removed from disk");
// disable if still uploading
} else if (state === STATES.UPLOAD) {
displayBtnData.disabled = true;
displayBtnData.title = _l("This dataset must finish uploading before it can be viewed");
// disable if still new
} else if (state === STATES.NEW) {
displayBtnData.disabled = true;
displayBtnData.title = _l("This dataset is not yet viewable");
} else {
displayBtnData.title = _l("View data");
// default link for dataset
displayBtnData.href = this.model.urls.display;
// add frame manager option onclick event
var self = this;
displayBtnData.onclick = (ev) => {
const Galaxy = getGalaxyInstance();
if (Galaxy.frame && Galaxy.frame.active) {
// Add dataset to frames.
const identifier = self.model.get("element_id") || self.model.get("id");
const url = `${getAppRoot()}datasets/${identifier}/display/?preview=True`;
Galaxy.frame.add({ url });
ev.preventDefault();
}
};
}
displayBtnData.faIcon = "fa-eye";
return faIconButton(displayBtnData);
},
// ......................................................................... rendering details
/** Render the enclosing div of the hda body and, if expanded, the html in the body
* @returns {jQuery} rendered DOM
*/
_renderDetails: function () {
//TODO: generalize to be allow different details for each state
// no access - render nothing but a message
if (this.model.get("state") === STATES.NOT_VIEWABLE) {
return $(this.templates.noAccess(this.model.toJSON(), this));
}
var $details = _super.prototype._renderDetails.call(this);
$details.find(".actions .left").empty().append(this._renderSecondaryActions());
$details.find(".summary").html(this._renderSummary()).prepend(this._renderDetailMessages());
$details.find(".display-applications").html(this._renderDisplayApplications());
this._setUpBehaviors($details);
return $details;
},
/** Defer to the appropo summary rendering fn based on state */
_renderSummary: function () {
var json = this.model.toJSON();
var summaryRenderFn = this.templates.summaries[json.state];
summaryRenderFn = summaryRenderFn || this.templates.summaries.unknown;
return summaryRenderFn(json, this);
},
/** Render messages to be displayed only when the details are shown */
_renderDetailMessages: function () {
var view = this;
var $warnings = $('<div class="detail-messages"></div>');
var json = view.model.toJSON();
//TODO:! unordered (map)
_.each(view.templates.detailMessages, (templateFn) => {
$warnings.append($(templateFn(json, view)));
});
return $warnings;
},
/** Render the external display application links */
_renderDisplayApplications: function () {
if (this.model.isDeletedOrPurged()) {
return "";
}
// render both old and new display apps using the same template
return [
this.templates.displayApplications(this.model.get("display_apps"), this),
this.templates.displayApplications(this.model.get("display_types"), this),
].join("");
},
// ......................................................................... secondary/details actions
/** A series of links/buttons for less commonly used actions: re-run, info, etc. */
_renderSecondaryActions: function () {
this.debug("_renderSecondaryActions");
switch (this.model.get("state")) {
case STATES.NOT_VIEWABLE:
return [];
case STATES.OK:
case STATES.FAILED_METADATA:
case STATES.ERROR:
return [
this._renderDownloadButton(),
this._renderClipboardButton(),
this._renderShowParamsButton(),
];
}
return [this._renderShowParamsButton()];
},
/** Render icon-button to show the input and output (stdout/err) for the job that created this.
* @returns {jQuery} rendered DOM
*/
_renderShowParamsButton: function () {
return faIconButton({
title: _l("View details"),
classes: "params-btn",
href: this.model.urls.show_params,
target: this.linkTarget,
faIcon: "fa-info-circle",
onclick: (ev) => {
const Galaxy = getGalaxyInstance();
const showDetailsUrl = `/datasets/${this.model.get("element_id") || this.model.get("id")}/details`;
if (Galaxy.frame && Galaxy.frame.active) {
ev.preventDefault();
Galaxy.frame.add({
url: showDetailsUrl,
title: `Dataset Details of ${this.model.get("name")}`,
});
} else if (Galaxy.router) {
ev.preventDefault();
Galaxy.router.push(showDetailsUrl);
Galaxy.trigger("activate-hda", this.model.get("element_id") || this.model.get("id"));
}
},
});
},
isPurged: function () {
// don't show anything if the data's been purged
return !!(this.model.get("purged") || !this.model.hasData());
},
/** Render icon-button/popupmenu to download the data (and/or the associated meta files (bai, etc.)) for this.
* @returns {jQuery} rendered DOM
*/
_renderClipboardButton: function () {
const isUnsharable =
this.model.attributes.permissions &&
this.model.attributes.permissions.access.length === 1 &&
this.model.attributes.permissions.access.includes(getGalaxyInstance().user.id);
var urls = this.model.urls;
if (!this.isPurged() && urls.download) {
let title = _l("Copy link");
let style = "";
if (isUnsharable) {
title = _l("Preferences restrict sharing");
style = "opacity: 0.3";
}
return faIconButton({
faIcon: "fa-chain",
style: style,
title: title,
onclick: function () {
if (!isUnsharable) {
navigator.clipboard.writeText(`${window.location.origin}${urls.download}`).then(() => {
Toast.info("Link is copied to your clipboard");
});
} else {
Toast.warning("Dataset is not sharable");
}
},
});
}
},
/** Render icon-button/popupmenu to download the data (and/or the associated meta files (bai, etc.)) for this.
* @returns {jQuery} rendered DOM
*/
_renderDownloadButton: function () {
if (this.isPurged()) {
return null;
}
// return either: a popupmenu with links to download assoc. meta files (if there are meta files)
// or a single download icon-button (if there are no meta files)
if (!_.isEmpty(this.model.get("meta_files"))) {
return this._renderMetaFileDownloadButton();
}
return $(`
<a class="download-btn icon-btn" href="${this.model.urls.download}" title="${_l("Download")}">
<span class="fa fa-floppy-o"></span>
</a>`);
},
/** Render the download button which opens a dropdown with links to download assoc. meta files (indices, etc.) */
_renderMetaFileDownloadButton: function () {
var urls = this.model.urls;
return $(`
<div class="metafile-dropdown dropdown">
<a class="download-btn icon-btn" href="${urls.download}" data-toggle="dropdown" title="${_l(
"Download"
)}">
<span class="fa fa-floppy-o"></span>
</a>
<div class="dropdown-menu" role="menu">
<a class="dropdown-item" href="${urls.download}">
${_l("Download dataset")}
</a>
${_.map(
this.model.get("meta_files"),
(meta_file) =>
`<a class="dropdown-item" href="${
urls.meta_download + meta_file.file_type
}" data-description="download ${meta_file.file_type}">
${_l("Download")} ${meta_file.file_type}
</a>`
)}
</div>
</div>`);
},
// ......................................................................... misc
events: _.extend(_.clone(_super.prototype.events), {
"click .display-btn": function (ev) {
this.trigger("display", this, ev);
},
"click .params-btn": function (ev) {
this.trigger("params", this, ev);
},
"click .download-btn": function (ev) {
this.trigger("download", this, ev);
},
}),
// ......................................................................... misc
/** String representation */
toString: function () {
var modelString = this.model ? `${this.model}` : "(no model)";
return `DatasetListItemView(${modelString})`;
},
}
);
// ............................................................................ TEMPLATES
/** underscore templates */
DatasetListItemView.prototype.templates = (() => {
//TODO: move to require text! plugin
var warnings = _.extend({}, _super.prototype.templates.warnings, {
failed_metadata: BASE_MVC.wrapTemplate([
// failed metadata is rendered as a warning on an otherwise ok dataset view
'<% if( model.state === "failed_metadata" ){ %>',
'<div class="warningmessagesmall">',
_l("An error occurred setting the metadata for this dataset"),
"</div>",
"<% } %>",
]),
error: BASE_MVC.wrapTemplate([
// error during index fetch - show error on dataset
"<% if( model.error ){ %>",
'<div class="errormessagesmall">',
_l("There was an error getting the data for this dataset"),
": <%- model.error %>",
"</div>",
"<% } %>",
]),
purged: BASE_MVC.wrapTemplate([
"<% if( model.purged ){ %>",
'<div class="purged-msg warningmessagesmall">',
_l("This dataset has been deleted and removed from disk"),
"</div>",
"<% } %>",
]),
deleted: BASE_MVC.wrapTemplate([
// deleted not purged
"<% if( model.deleted && !model.purged ){ %>",
'<div class="deleted-msg warningmessagesmall">',
_l("This dataset has been deleted"),
"</div>",
"<% } %>",
]),
//NOTE: hidden warning is only needed for HDAs
});
var detailsTemplate = BASE_MVC.wrapTemplate(
[
'<div class="details">',
'<div class="summary"></div>',
'<div class="actions clear">',
'<div class="left"></div>',
'<div class="right"></div>',
"</div>",
// do not display tags, annotation, display apps, or peek when deleted
"<% if( !dataset.deleted && !dataset.purged ){ %>",
'<div class="tags-display"></div>',
'<div class="annotation-display"></div>',
'<div class="display-applications"></div>',
"<% if( dataset.peek ){ %>",
'<pre class="dataset-peek"><%= dataset.peek %></pre>',
"<% } %>",
"<% } %>",
"</div>",
],
"dataset"
);
var noAccessTemplate = BASE_MVC.wrapTemplate(
[
'<div class="details">',
'<div class="summary">',
_l("You do not have permission to view this dataset"),
"</div>",
"</div>",
],
"dataset"
);
//TODO: still toooooooooooooo complex - rework
var summaryTemplates = {};
summaryTemplates[STATES.OK] = summaryTemplates[STATES.FAILED_METADATA] = BASE_MVC.wrapTemplate(
[
"<% if( dataset.misc_blurb ){ %>",
'<div class="blurb">',
'<span class="value"><%- dataset.misc_blurb %></span>',
"</div>",
"<% } %>",
"<% if( dataset.file_ext ){ %>",
'<div class="datatype">',
'<label class="prompt">',
_l("format"),
"</label>",
'<span class="value"><%- dataset.file_ext %></span>',
"</div>",
"<% } %>",
"<% if( dataset.metadata_dbkey ){ %>",
'<div class="dbkey">',
'<label class="prompt">',
_l("database"),
"</label>",
'<span class="value">',
"<%- dataset.metadata_dbkey %>",
"</span>",
"</div>",
"<% } %>",
"<% if( dataset.misc_info ){ %>",
'<div class="info">',
'<span class="value"><%- dataset.misc_info %></span>',
"</div>",
"<% } %>",
],
"dataset"
);
summaryTemplates[STATES.NEW] = BASE_MVC.wrapTemplate(
["<div>", _l("This is a new dataset and not all of its data are available yet"), "</div>"],
"dataset"
);
summaryTemplates[STATES.NOT_VIEWABLE] = BASE_MVC.wrapTemplate(
["<div>", _l("You do not have permission to view this dataset"), "</div>"],
"dataset"
);
summaryTemplates[STATES.DISCARDED] = BASE_MVC.wrapTemplate(
[
"<div>",
_l(
"This dataset is discarded - the job creating it may have been cancelled or it may have been imported with file data"
),
"</div>",
],
"dataset"
);
summaryTemplates[STATES.DEFERRED] = BASE_MVC.wrapTemplate(
[
"<div>",
_l("This dataset is remote, has not be ingested by Galaxy, and full metadata may not be available"),
"</div>",
],
"dataset"
);
summaryTemplates[STATES.QUEUED] = BASE_MVC.wrapTemplate(
["<div>", _l("This job is waiting to run"), "</div>"],
"dataset"
);
summaryTemplates[STATES.RUNNING] = BASE_MVC.wrapTemplate(
["<div>", _l("This job is currently running"), "</div>"],
"dataset"
);
summaryTemplates[STATES.UPLOAD] = BASE_MVC.wrapTemplate(
["<div>", _l("This dataset is currently uploading"), "</div>"],
"dataset"
);
summaryTemplates[STATES.SETTING_METADATA] = BASE_MVC.wrapTemplate(
["<div>", _l("Metadata is being auto-detected"), "</div>"],
"dataset"
);
summaryTemplates[STATES.PAUSED] = BASE_MVC.wrapTemplate(
[
"<div>",
_l('This job is paused. Use the "Resume Paused Jobs" in the history menu to resume'),
"</div>",
'<div class="info"><%- dataset.misc_info %></div>',
],
"dataset"
);
summaryTemplates[STATES.ERROR] = BASE_MVC.wrapTemplate(
[
"<% if( !dataset.purged ){ %>",
"<div><%- dataset.misc_blurb %></div>",
"<% } %>",
'<span class="help-text">',
_l("An error occurred with this dataset"),
":</span>",
'<div class="job-error-text"><%- dataset.misc_info %></div>',
],
"dataset"
);
summaryTemplates[STATES.EMPTY] = BASE_MVC.wrapTemplate(
["<div>", _l("No data"), ": <i><%- dataset.misc_blurb %></i></div>"],
"dataset"
);
summaryTemplates.unknown = BASE_MVC.wrapTemplate(
['<div>Error: unknown dataset state: "<%- dataset.state %>"</div>'],
"dataset"
);
// messages to be displayed only within the details section ('below the fold')
var detailMessageTemplates = {
resubmitted: BASE_MVC.wrapTemplate([
// deleted not purged
"<% if( model.resubmitted ){ %>",
'<div class="resubmitted-msg alert alert-info">',
_l("The job creating this dataset has been resubmitted"),
"</div>",
"<% } %>",
]),
};
// this is applied to both old and new style display apps
var displayApplicationsTemplate = BASE_MVC.wrapTemplate(
[
"<% _.each( apps, function( app ){ %>",
'<div class="display-application">',
'<span class="display-application-location"><%- app.label %></span> ',
'<span class="display-application-links">',
"<% _.each( app.links, function( link ){ %>",
'<a target="<%- link.target %>" href="<%- link.href %>">',
"<% print( _l( link.text ) ); %>",
"</a> ",
"<% }); %>",
"</span>",
"</div>",
"<% }); %>",
],
"apps"
);
return _.extend({}, _super.prototype.templates, {
warnings: warnings,
details: detailsTemplate,
noAccess: noAccessTemplate,
summaries: summaryTemplates,
detailMessages: detailMessageTemplates,
displayApplications: displayApplicationsTemplate,
});
})();
// ============================================================================
export default {
DatasetListItemView: DatasetListItemView,
};
-42
View File
@@ -1,42 +0,0 @@
import LIST_VIEW from "mvc/list/list-view";
import DATASET_LI from "mvc/dataset/dataset-li";
import _l from "utils/localization";
var logNamespace = "dataset";
/* =============================================================================
TODO:
============================================================================= */
var _super = LIST_VIEW.ListPanel;
/** @class non-editable, read-only View/Controller for a list of datasets.
*/
var DatasetList = _super.extend(
/** @lends DatasetList.prototype */ {
_logNamespace: logNamespace,
/** class to use for constructing the sub-views */
viewClass: DATASET_LI.DatasetListItemView,
className: `${_super.prototype.className} dataset-list`,
/** string to no hdas match the search terms */
noneFoundMsg: _l("No matching datasets found"),
// ......................................................................... SET UP
/** Set up the view, set up storage, bind listeners to HistoryContents events
* @param {Object} attributes optional settings for the panel
*/
initialize: function (attributes) {
_super.prototype.initialize.call(this, attributes);
},
/** Return a string rep of the history */
toString: function () {
return `DatasetList(${this.collection})`;
},
}
);
//==============================================================================
export default {
DatasetList: DatasetList,
};
-390
View File
@@ -1,390 +0,0 @@
import _ from "underscore";
import jQuery from "jquery";
import Backbone from "backbone";
import { getAppRoot } from "onload/loadConfig";
import STATES from "mvc/dataset/states";
import BASE_MVC from "mvc/base-mvc";
import _l from "utils/localization";
import TAB_UPDATES from "mvc/user/tab-updates";
var logNamespace = "dataset";
var hiddenupdates = 1;
//==============================================================================
var searchableMixin = BASE_MVC.SearchableModelMixin;
/** @class base model for any DatasetAssociation (HDAs, LDDAs, DatasetCollectionDAs).
* No knowledge of what type (HDA/LDDA/DCDA) should be needed here.
* The DA's are made searchable (by attribute) by mixing in SearchableModelMixin.
*/
var DatasetAssociation = Backbone.Model.extend(BASE_MVC.LoggableMixin).extend(
BASE_MVC.mixin(
searchableMixin,
/** @lends DatasetAssociation.prototype */ {
_logNamespace: logNamespace,
/** default attributes for a model */
defaults: {
state: STATES.NEW,
deleted: false,
purged: false,
name: "(unnamed dataset)",
accessible: true,
// sniffed datatype (sam, tabular, bed, etc.)
data_type: "",
file_ext: "",
file_size: 0,
// array of associated file types (eg. [ 'bam_index', ... ])
meta_files: [],
misc_blurb: "",
misc_info: "",
tags: [],
// do NOT default on annotation, as this default is valid and will be passed on 'save'
// which is incorrect behavior when the model is only partially fetched (annos are not passed in summary data)
//annotation : ''
},
/** instance vars and listeners */
initialize: function (attributes, options) {
this.debug(`${this}(Dataset).initialize`, attributes, options);
//!! this state is not in trans.app.model.Dataset.states - set it here -
if (!this.get("accessible")) {
this.set("state", STATES.NOT_VIEWABLE);
}
/** Datasets rely/use some web controllers - have the model generate those URLs on startup */
this.urls = this._generateUrls();
this._setUpListeners();
},
_getDatasetId: function () {
return this.get("id");
},
/** returns misc. web urls for rendering things like re-run, display, etc. */
_generateUrls: function () {
const id = this._getDatasetId();
if (!id) {
return {};
}
var urls = {
purge: `datasets/${id}/purge_async`,
display: `datasets/${id}/display/?preview=True`,
edit: `datasets/edit?dataset_id=${id}`,
download: `api/datasets/${id}/display${this._downloadQueryParameters()}`,
report_error: `datasets/error?dataset_id=${id}`,
rerun: `tool_runner/rerun?id=${id}`,
show_params: `datasets/${id}/details`,
visualization: "visualization",
meta_download: `dataset/get_metadata_file?hda_id=${id}&metadata_name=`,
};
_.each(urls, (value, key) => {
urls[key] = getAppRoot() + value;
});
this.urls = urls;
return urls;
},
_downloadQueryParameters: function () {
return `?to_ext=${this.get("file_ext")}`;
},
/** 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"));
if (newState != "discarded") {
if (newState === "ok") {
// If Notifications are supported, send one.
if (window.Notification) {
new Notification(`Job complete: ${this.get("name")}`, {
icon: "static/favicon.ico",
});
}
if (TAB_UPDATES.is_hidden()) {
TAB_UPDATES.hidden_count(hiddenupdates);
hiddenupdates++;
}
} else if (newState == "error") {
// If Notifications are supported, send one.
if (window.Notification) {
new Notification(`Job failure: ${this.get("name")}`, {
icon: "static/erricon.ico",
});
if (TAB_UPDATES.is_hidden() && Notification.permission == "granted") {
TAB_UPDATES.change_favicon("static/erricon.ico");
}
}
}
}
}
});
// the download url (currently) relies on having a correct file extension
this.on("change:id change:file_ext", function (currModel) {
this._generateUrls();
});
},
// ........................................................................ common queries
/** override to add urls */
toJSON: function () {
var json = Backbone.Model.prototype.toJSON.call(this);
//console.warn( 'returning json?' );
//return json;
return _.extend(json, {
urls: this.urls,
});
},
/** Is this dataset deleted or purged? */
isDeletedOrPurged: function () {
return this.get("deleted") || this.get("purged");
},
/** Is this dataset in a 'ready' state; where 'Ready' states are states where no
* processing (for the ds) is left to do on the server.
*/
inReadyState: function () {
var ready = _.contains(STATES.READY_STATES, this.get("state"));
return this.isDeletedOrPurged() || ready;
},
/** Does this model already contain detailed data (as opposed to just summary level data)? */
hasDetails: function () {
// if it's inaccessible assume it has everything it needs
if (!this.get("accessible")) {
return true;
}
return this.has("annotation");
},
/** Convenience function to match dataset.has_data. */
hasData: function () {
return this.get("file_size") > 0;
},
// ........................................................................ ajax
fetch: function (options) {
var dataset = this;
return Backbone.Model.prototype.fetch.call(this, options).always(() => {
dataset._generateUrls();
});
},
/** override to use actual Dates objects for create/update times */
parse: function (response, options) {
var parsed = Backbone.Model.prototype.parse.call(this, response, options);
if (parsed.create_time) {
parsed.create_time = new Date(parsed.create_time);
}
if (parsed.update_time) {
parsed.update_time = new Date(parsed.update_time);
}
return parsed;
},
/** override to wait by default */
save: function (attrs, options) {
options = options || {};
options.wait = _.isUndefined(options.wait) ? true : options.wait;
return Backbone.Model.prototype.save.call(this, attrs, options);
},
//NOTE: subclasses of DA's will need to implement url and urlRoot in order to have these work properly
/** save this dataset, _Mark_ing it as deleted (just a flag) */
delete: function (options) {
if (this.get("deleted")) {
return jQuery.when();
}
return this.save({ deleted: true }, options);
},
/** save this dataset, _Mark_ing it as undeleted */
undelete: function (options) {
if (!this.get("deleted") || this.get("purged")) {
return jQuery.when();
}
return this.save({ deleted: false }, options);
},
/** remove the file behind this dataset from the filesystem (if permitted) */
purge: function _purge(options) {
//TODO: use, override model.destroy, HDA.delete({ purge: true })
if (this.get("purged")) {
return jQuery.when();
}
options = options || {};
options.url = this.urls.purge;
//TODO: ideally this would be a DELETE call to the api
// using purge async for now
var hda = this;
var xhr = jQuery.ajax(options);
xhr.done((message, status, responseObj) => {
hda.set({ deleted: true, purged: true });
});
xhr.fail((xhr, status, message) => {
// Exception messages are hidden within error page including: '...not allowed in this Galaxy instance.'
// unbury and re-add to xhr
var error = _l("Unable to purge dataset");
var messageBuriedInUnfortunatelyFormattedError =
"Removal of datasets by users " + "is not allowed in this Galaxy instance";
if (xhr.responseJSON && xhr.responseJSON.error) {
error = xhr.responseJSON.error;
} else if (xhr.responseText.indexOf(messageBuriedInUnfortunatelyFormattedError) !== -1) {
error = messageBuriedInUnfortunatelyFormattedError;
}
xhr.responseText = error;
hda.trigger("error", hda, xhr, options, _l(error), {
error: error,
});
});
return xhr;
},
// ........................................................................ searching
/** what attributes of an HDA will be used in a text search */
searchAttributes: ["name", "file_ext", "genome_build", "misc_blurb", "misc_info", "annotation", "tags"],
/** our attr keys don't often match the labels we display to the user - so, when using
* attribute specifiers ('name="bler"') in a term, allow passing in aliases for the
* following attr keys.
*/
searchAliases: {
title: "name",
format: "file_ext",
database: "genome_build",
blurb: "misc_blurb",
description: "misc_blurb",
info: "misc_info",
tag: "tags",
},
// ........................................................................ misc
/** String representation */
toString: function () {
var nameAndId = this.get("id") || "";
if (this.get("name")) {
nameAndId = `"${this.get("name")}",${nameAndId}`;
}
return `Dataset(${nameAndId})`;
},
}
)
);
//==============================================================================
/** @class Backbone collection for dataset associations.
*/
var DatasetAssociationCollection = Backbone.Collection.extend(BASE_MVC.LoggableMixin).extend(
/** @lends HistoryContents.prototype */ {
_logNamespace: logNamespace,
model: DatasetAssociation,
/** root api url */
urlRoot: `${getAppRoot()}api/datasets`,
/** url fn */
url: function () {
return this.urlRoot;
},
// ........................................................................ common queries
/** Get the ids of every item in this collection
* @returns array of encoded ids
*/
ids: function () {
return this.map((item) => item.get("id"));
},
/** Get contents that are not ready
* @returns array of content models
*/
notReady: function () {
return this.filter((content) => !content.inReadyState());
},
/** return true if any datasets don't have details */
haveDetails: function () {
return this.all((dataset) => dataset.hasDetails());
},
// ........................................................................ ajax
/** using a queue, perform ajaxFn on each of the models in this collection */
ajaxQueue: function (ajaxFn, options) {
var deferred = jQuery.Deferred();
var startingLength = this.length;
var responses = [];
if (!startingLength) {
deferred.resolve([]);
return deferred;
}
// use reverse order (stylistic choice)
var ajaxFns = this.chain()
.reverse()
.map((dataset, i) => () => {
var xhr = ajaxFn.call(dataset, options);
// if successful, notify using the deferred to allow tracking progress
xhr.done((response) => {
deferred.notify({
curr: i,
total: startingLength,
response: response,
model: dataset,
});
});
// (regardless of previous error or success) if not last ajax call, shift and call the next
// if last fn, resolve deferred
xhr.always((response) => {
responses.push(response);
if (ajaxFns.length) {
ajaxFns.shift()();
} else {
deferred.resolve(responses);
}
});
})
.value();
// start the queue
ajaxFns.shift()();
return deferred;
},
// ........................................................................ sorting/filtering
/** return a new collection of datasets whose attributes contain the substring matchesWhat */
matches: function (matchesWhat) {
return this.filter((dataset) => dataset.matches(matchesWhat));
},
/** String representation. */
toString: function () {
return ["DatasetAssociationCollection(", this.length, ")"].join("");
},
}
);
window.addEventListener("focus", function () {
const testing = document.getElementById("tabicon");
if (testing && testing.href.search("erricon") != -1) {
TAB_UPDATES.change_favicon("static/favicon.ico");
}
document.title = "Galaxy";
hiddenupdates = 1;
});
//==============================================================================
export default {
DatasetAssociation: DatasetAssociation,
DatasetAssociationCollection: DatasetAssociationCollection,
};