diff --git a/client/src/assets/images/largespinner.gif b/client/src/assets/images/largespinner.gif deleted file mode 100644 index 3288d1035d70..000000000000 Binary files a/client/src/assets/images/largespinner.gif and /dev/null differ diff --git a/client/src/components/Visualizations/Tabular/TabularChunkedView.vue b/client/src/components/Visualizations/Tabular/TabularChunkedView.vue new file mode 100644 index 000000000000..c5c1cfddcdb2 --- /dev/null +++ b/client/src/components/Visualizations/Tabular/TabularChunkedView.vue @@ -0,0 +1,199 @@ + + + + + diff --git a/client/src/mvc/dataset/data.js b/client/src/mvc/dataset/data.js index 28af32d7e2b0..2705f3a69606 100644 --- a/client/src/mvc/dataset/data.js +++ b/client/src/mvc/dataset/data.js @@ -1,12 +1,10 @@ import _ from "underscore"; -import $ from "jquery"; import Backbone from "backbone"; -import { parse } from "csv-parse/sync"; - import { getAppRoot } from "onload/loadConfig"; -import _l from "utils/localization"; -import mod_icon_btn from "mvc/ui/icon-button"; -import { getGalaxyInstance } from "app"; + +//temporary +import { appendVueComponent } from "utils/mountVueComponent"; +import TabularChunkedView from "components/Visualizations/Tabular/TabularChunkedView.vue"; /** * Dataset metedata. @@ -68,575 +66,11 @@ export var Dataset = Backbone.Model.extend({ urlRoot: `${getAppRoot()}api/datasets`, }); -/** - * A tabular dataset. This object extends dataset to provide incremental chunked data. - */ -export var TabularDataset = Dataset.extend({ - defaults: _.extend({}, Dataset.prototype.defaults, { - chunk_url: null, - first_data_chunk: null, - offset: 0, - at_eof: false, - }), - - initialize: function (options) { - Dataset.prototype.initialize.call(this); - - // If first data chunk is available, next chunk is 1. - if (this.attributes.first_data_chunk) { - this.attributes.offset = this.attributes.first_data_chunk.offset; - } - this.attributes.chunk_url = `${getAppRoot()}dataset/display?dataset_id=${this.id}`; - this.attributes.url_viz = `${getAppRoot()}visualization`; - }, - - /** - * Returns a jQuery Deferred object that resolves to the next data chunk or null if at EOF. - */ - get_next_chunk: function () { - // If already at end of file, do nothing. - if (this.attributes.at_eof) { - return null; - } - - // Get next chunk. - var self = this; - - var next_chunk = $.Deferred(); - $.getJSON(this.attributes.chunk_url, { - offset: self.attributes.offset, - }).success((chunk) => { - var rval; - if (chunk.ck_data !== "") { - // Found chunk. - rval = chunk; - self.attributes.offset = chunk.offset; - } else { - // At EOF. - self.attributes.at_eof = true; - rval = null; - } - next_chunk.resolve(rval); - }); - - return next_chunk; - }, -}); - export var DatasetCollection = Backbone.Collection.extend({ model: Dataset, }); -/** - * Provides a base for table-based, dynamic view of a tabular dataset. - * Do not instantiate directly; use either TopLevelTabularDatasetChunkedView - * or EmbeddedTabularDatasetChunkedView. - */ -var TabularDatasetChunkedView = Backbone.View.extend({ - /** - * Initialize view and, importantly, set a scroll element. - */ - initialize: function (options) { - // Row count for rendering. - this.row_count = 0; - // Flag for active loading - this.loading_chunk = false; - // Configure delimiter for parsing data - this.delimiter = this.model?.attributes?.file_ext === "csv" ? "," : "\t"; - this.parseArgs = { - delimiter: this.delimiter, - }; - // load trackster button - new TabularButtonTracksterView({ - model: options.model, - $el: this.$el, - }); - }, - - expand_to_container: function () { - if (this.$el.height() < this.scroll_elt.height()) { - this.attempt_to_fetch(); - } - }, - - attempt_to_fetch: function (func) { - var self = this; - if (!this.loading_chunk && this.scrolled_to_bottom()) { - this.loading_chunk = true; - this.loading_indicator.show(); - $.when(self.model.get_next_chunk()).then((result) => { - if (result) { - self._renderChunk(result); - self.loading_chunk = false; - } - self.loading_indicator.hide(); - self.expand_to_container(); - }); - } - }, - - render: function () { - // Add loading indicator. - this.loading_indicator = $("
").attr("id", "loading_indicator"); - this.$el.append(this.loading_indicator); - - // Add data table and header. - var data_table = $("").attr({ - id: "content_table", - cellpadding: 0, - }); - this.$el.append(data_table); - var column_names = this.model.get_metadata("column_names"); - var header_container = $("").appendTo(data_table); - var header_row = $("").appendTo(header_container); - if (column_names?.length > 0) { - header_row.append(``); - } else { - for (var j = 1; j <= this.model.get_metadata("columns"); j++) { - header_row.append(``); - } - } - - // Render first chunk. - var self = this; - - var first_chunk = this.model.get("first_data_chunk"); - if (first_chunk) { - // First chunk is bootstrapped, so render now. - this._renderChunk(first_chunk); - } else { - // No bootstrapping, so get first chunk and then render. - $.when(self.model.get_next_chunk()).then((result) => { - self._renderChunk(result); - }); - } - - // -- Show new chunks during scrolling. -- - - // Set up chunk loading when scrolling using the scrolling element. - this.scroll_elt.scroll(() => { - self.attempt_to_fetch(); - }); - }, - - /** - * Returns true if user has scrolled to the bottom of the view. - */ - scrolled_to_bottom: function () { - return false; - }, - - // -- Helper functions. -- - - _renderCell: function (cell_contents, index, colspan) { - var $cell = $(""); - var num_columns = this.model.get_metadata("columns"); - if (this.row_count % 2 !== 0) { - row.addClass("dark_row"); - } - - if (rowData.length === num_columns) { - _.each( - rowData, - function (cell_contents, index) { - row.append(this._renderCell(cell_contents, index)); - }, - this - ); - } else if (rowData.length > num_columns) { - // SAM file or like format with optional metadata included. - _.each( - rowData.slice(0, num_columns - 1), - function (cell_contents, index) { - row.append(this._renderCell(cell_contents, index)); - }, - this - ); - row.append(this._renderCell(rowData.slice(num_columns - 1).join("\t"), num_columns - 1)); - } else if (rowData.length === 1) { - // Try to split by comma first - let rowDataSplit = rowData[0].split(","); - if (rowDataSplit.length === num_columns) { - _.each( - rowDataSplit, - function (cell_contents, index) { - row.append(this._renderCell(cell_contents, index)); - }, - this - ); - } else { - rowDataSplit = rowData[0].split(" "); - if (rowDataSplit.length === num_columns) { - _.each( - rowDataSplit, - function (cell_contents, index) { - row.append(this._renderCell(cell_contents, index)); - }, - this - ); - } else { - // Comment line, just return the one cell. - row.append(this._renderCell(rowData[0], 0, num_columns)); - } - } - } else { - // rowData.length is greater than one, but less than num_columns. Render cells and pad tds. - // Possibly a SAM file or like format with optional metadata missing. - // Could also be a tabular file with a line with missing columns. - _.each( - rowData, - function (cell_contents, index) { - row.append(this._renderCell(cell_contents, index)); - }, - this - ); - _.each(_.range(num_columns - rowData.length), () => { - row.append($("
${column_names.join("")}Column ${j}").text(cell_contents); - var column_types = this.model.get_metadata("column_types"); - if (colspan !== undefined) { - $cell.attr("colspan", colspan).addClass("stringalign"); - } else if (column_types) { - if (index < column_types.length) { - if (column_types[index] === "str" || column_types[index] === "list") { - /* Left align all str columns, right align the rest */ - $cell.addClass("stringalign"); - } - } - } - return $cell; - }, - - _renderRow: function (rowData) { - var row = $("
")); - }); - } - - this.row_count++; - return row; - }, - - _renderChunk: function (chunk) { - var data_table = this.$el.find("table"); - let parsedChunk = []; - try { - parsedChunk = parse(chunk.ck_data, this.parseArgs); - } catch (error) { - // If this blows up it's likely data in a comment or header line - // (e.g. VCF files) so just split it by newline first then parse - // each line individually. - parsedChunk = chunk.ck_data.trim().split("\n"); - parsedChunk = parsedChunk.map((line) => { - try { - return parse(line, this.parseArgs)[0]; - } catch (error) { - // Failing lines get passed through intact for row-level - // rendering/parsing. - return [line]; - } - }); - } - _.each( - parsedChunk, - (rowData, index) => { - if (index >= (chunk.data_line_offset || 0)) { - data_table.append(this._renderRow(rowData)); - } - }, - this - ); - }, -}); - -/** - * Tabular view that is placed at the top level of page. Scrolling occurs - * view top-level elements outside of view. - */ -var TopLevelTabularDatasetChunkedView = TabularDatasetChunkedView.extend({ - initialize: function (options) { - TabularDatasetChunkedView.prototype.initialize.call(this, options); - - // Scrolling happens in top-level elements. - var scroll_elt = _.find(this.$el.parents(), (p) => $(p).css("overflow") === "auto"); - - // If no scrolling element found, use window. - if (!scroll_elt) { - scroll_elt = window; - } - - // Wrap scrolling element for easy access. - this.scroll_elt = $(scroll_elt); - }, - - /** - * Returns true if user has scrolled to the bottom of the view. - */ - scrolled_to_bottom: function () { - return this.$el.height() - this.scroll_elt.scrollTop() - this.scroll_elt.height() <= 0; - }, -}); - -/** - * Tabular view tnat is embedded in a page. Scrolling occurs in view's el. - */ -var EmbeddedTabularDatasetChunkedView = TabularDatasetChunkedView.extend({ - initialize: function (options) { - TabularDatasetChunkedView.prototype.initialize.call(this, options); - - // Because view is embedded, set up div to do scrolling. - this.scroll_elt = this.$el.css({ - position: "relative", - overflow: "scroll", - height: options.height || "500px", - }); - }, - - /** - * Returns true if user has scrolled to the bottom of the view. - */ - scrolled_to_bottom: function () { - return this.$el.scrollTop() + this.$el.innerHeight() >= this.el.scrollHeight; - }, -}); - -function search(str, array) { - for (var j = 0; j < array.length; j++) { - if (array[j].match(str)) { - return j; - } - } - return -1; -} - -/** Button for trackster visualization */ -var TabularButtonTracksterView = Backbone.View.extend({ - // gene region columns - col: { - chrom: null, - start: null, - end: null, - }, - - // url for trackster - url_viz: null, - - // dataset id - dataset_id: null, - - // database key - genome_build: null, - - // data type - file_ext: null, - - // backbone initialize - initialize: function (options) { - // check if environment is available - const Galaxy = getGalaxyInstance(); - - // link galaxy modal or create one - if (Galaxy && Galaxy.modal) { - this.modal = Galaxy.modal; - } - - // link galaxy frames - if (Galaxy && Galaxy.frame) { - this.frame = Galaxy.frame; - } - - // check - if (!this.modal || !this.frame) { - return; - } - - // model/metadata - var model = options.model; - var metadata = model.get("metadata"); - - // check for datatype - if (!model.get("file_ext")) { - return; - } - - // get data type - this.file_ext = model.get("file_ext"); - - // check for bed-file format - if (this.file_ext == "bed") { - // verify that metadata exists - if (metadata.get("chromCol") && metadata.get("startCol") && metadata.get("endCol")) { - // read in columns - this.col.chrom = metadata.get("chromCol") - 1; - this.col.start = metadata.get("startCol") - 1; - this.col.end = metadata.get("endCol") - 1; - } else { - console.log("TabularButtonTrackster : Bed-file metadata incomplete."); - return; - } - } - - // check for vcf-file format - if (this.file_ext == "vcf") { - // search array - - // load - this.col.chrom = search("Chrom", metadata.get("column_names")); - this.col.start = search("Pos", metadata.get("column_names")); - this.col.end = null; - - // verify that metadata exists - if (this.col.chrom == -1 || this.col.start == -1) { - console.log("TabularButtonTrackster : VCF-file metadata incomplete."); - return; - } - } - - // check - if (this.col.chrom === undefined) { - return; - } - - // get dataset id - if (model.id) { - this.dataset_id = model.id; - } else { - console.log("TabularButtonTrackster : Dataset identification is missing."); - return; - } - - // get url - if (model.get("url_viz")) { - this.url_viz = model.get("url_viz"); - } else { - console.log("TabularButtonTrackster : Url for visualization controller is missing."); - return; - } - - // get genome_build / database key - if (model.get("genome_build")) { - this.genome_build = model.get("genome_build"); - } - - // create the icon - var btn_viz = new mod_icon_btn.IconButtonView({ - model: new mod_icon_btn.IconButton({ - title: _l("Visualize"), - icon_class: "chart_curve", - id: "btn_viz", - }), - }); - - // set element - this.setElement(options.$el); - - // add to element - this.$el.append(btn_viz.render().$el); - - // hide the button - this.hide(); - }, - - /** Add event handlers */ - events: { - "mouseover tr": "show", - mouseleave: "hide", - }, - - // show button - show: function (e) { - var self = this; - - // is numeric - function is_numeric(n) { - return !isNaN(parseFloat(n)) && isFinite(n); - } - - // check - if (this.col.chrom === null) { - return; - } - - // get selected data line - var row = $(e.target).parent(); - - // verify that location has been found - var chrom = row.children().eq(this.col.chrom).html(); - var start = row.children().eq(this.col.start).html(); - - // end is optional - var end = this.col.end ? row.children().eq(this.col.end).html() : start; - - // double check location - if (!chrom.match("^#") && chrom !== "" && is_numeric(start)) { - // get target gene region - var btn_viz_pars = { - dataset_id: this.dataset_id, - gene_region: `${chrom}:${start}-${end}`, - }; - - // get button position - var offset = row.offset(); - var left = offset.left - 10; - var top = offset.top - $(window).scrollTop() + 3; - - // update css - $("#btn_viz").css({ - position: "fixed", - top: `${top}px`, - left: `${left}px`, - }); - $("#btn_viz").off("click"); - $("#btn_viz").click(() => { - self.frame.add({ - title: _l("Trackster"), - url: `${self.url_viz}/trackster?${$.param(btn_viz_pars)}`, - }); - }); - - // show the button - $("#btn_viz").show(); - } else { - // hide the button - $("#btn_viz").hide(); - } - }, - - /** hide button */ - hide: function () { - this.$("#btn_viz").hide(); - }, -}); - -/** - * Create a tabular dataset chunked view (and requisite tabular dataset model) - * and appends to parent_elt. - */ -export var createTabularDatasetChunkedView = (options) => { - // If no model, create and set model from dataset config. - if (!options.model) { - options.model = new TabularDataset(options.dataset_config); - } - - var parent_elt = options.parent_elt; - var embedded = options.embedded; - - // Clean up options so that only needed options are passed to view. - delete options.embedded; - delete options.parent_elt; - delete options.dataset_config; - - // Create and set up view. - var view = embedded - ? new EmbeddedTabularDatasetChunkedView(options) - : new TopLevelTabularDatasetChunkedView(options); - view.render(); - - if (parent_elt) { - parent_elt.append(view.el); - // If we're sticking this in another element, once it's appended check - // to make sure we've filled enough space. - // Without this, the scroll elements don't work. - view.expand_to_container(); - } - - return view; +export const createTabularDatasetChunkedView = (options) => { + // We'll always have a parent_elt in options, so create a div and insert it into that. + return appendVueComponent(options.parent_elt, TabularChunkedView, { options }); }; diff --git a/client/src/style/scss/unsorted.scss b/client/src/style/scss/unsorted.scss index 09f168bfe32b..30367852fdeb 100644 --- a/client/src/style/scss/unsorted.scss +++ b/client/src/style/scss/unsorted.scss @@ -736,39 +736,6 @@ div.toolSectionTitle { } } -#loading_indicator { - position: fixed; - right: 10px; - top: 10px; - height: 32px; - width: 32px; - display: none; - background: url(../../assets/images/largespinner.gif); -} - -#content_table td { - text-align: right; - white-space: nowrap; - padding: 2px 10px; -} - -#content_table th { - white-space: nowrap; - padding: 2px 10px; -} - -#content_table td.stringalign { - text-align: left; -} - -#content_table .dark_row { - background-color: #ddd; -} - -#content_table th { - background-color: #aaa; -} - // ==== Integrated tool form styles .toolMenuAndView .toolForm { @@ -992,14 +959,3 @@ body.reports { margin-left: 10px; } } - -/* Used in Galaxy IE for spinner loading TODO: Remove in IE entrypoint refactoring */ -#ie-loading-spinner { - position: absolute; - margin: auto; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: url(../../assets/images/largespinner.gif) no-repeat center center fixed; -} diff --git a/client/src/utils/mountVueComponent.js b/client/src/utils/mountVueComponent.js index 8ef37f8e9dc8..02efe8e2e5bd 100644 --- a/client/src/utils/mountVueComponent.js +++ b/client/src/utils/mountVueComponent.js @@ -33,8 +33,9 @@ export const mountVueComponent = (ComponentDefinition) => { }; export const appendVueComponent = ($el, ComponentDefinition, propsData = {}) => { + // TODO: this doesn't append, it wipes out the element and replaces contents? const container = document.createElement("div"); - $el.empty().append(container); + $el.replaceChildren(container); const component = Vue.extend(ComponentDefinition); const mountFn = (propsData, el) => new component({ propsData, el }); return mountFn(propsData, container);