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 @@
+
+
+
+
+
+
+
+
+ {{ column || `Column ${index + 1}` }}
+
+
+
+
+
+ {{ element }}
+
+
+
+
+
+
+
+
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(`${column_names.join(" | ")} | `);
- } else {
- for (var j = 1; j <= this.model.get_metadata("columns"); j++) {
- header_row.append(`Column ${j} | `);
- }
- }
-
- // 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 = $("").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 = $(" | ");
- 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($(""));
- });
- }
-
- 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);
|