diff --git a/client/galaxy/scripts/components/DataDialog.vue b/client/galaxy/scripts/components/DataDialog.vue deleted file mode 100644 index 6e30d3a5c870..000000000000 --- a/client/galaxy/scripts/components/DataDialog.vue +++ /dev/null @@ -1,232 +0,0 @@ - - - - diff --git a/client/galaxy/scripts/components/DataDialog/DataDialog.test.js b/client/galaxy/scripts/components/DataDialog/DataDialog.test.js new file mode 100644 index 000000000000..682ff54a4a46 --- /dev/null +++ b/client/galaxy/scripts/components/DataDialog/DataDialog.test.js @@ -0,0 +1,180 @@ +import sinon from "sinon"; +import { mount } from "@vue/test-utils"; +import DataDialog from "./DataDialog.vue"; +import { __RewireAPI__ as rewire } from "./DataDialog"; +import { Model } from "./model.js"; +import { UrlTracker } from "./utilities.js"; +import { Services } from "./services"; +import Vue from "vue"; + +const mockOptions = { + callback: () => {}, + host: "host", + root: "root", + history: "history" +}; + +describe("model.js", () => { + let result = null; + it("Model operations for single, no format", () => { + let model = new Model(); + try { + model.add({ idx: 1 }); + throw "Accepted invalid record."; + } catch (error) { + expect(error).to.equals("Invalid record with no ."); + } + model.add({ id: 1 }); + expect(model.count()).to.equals(1); + expect(model.exists(1)).to.equals(true); + model.add({ id: 2, tag: "tag" }); + expect(model.count()).to.equals(1); + expect(model.exists(1)).to.equals(false); + expect(model.exists(2)).to.equals(true); + result = model.finalize(); + expect(result.id).to.equals(2); + expect(result.tag).to.equals("tag"); + }); + it("Model operations for multiple, with format", () => { + let model = new Model({ multiple: true, format: "tag" }); + model.add({ id: 1, tag: "tag_1" }); + expect(model.count()).to.equals(1); + model.add({ id: 2, tag: "tag_2" }); + expect(model.count()).to.equals(2); + result = model.finalize(); + expect(result.length).to.equals(2); + expect(result[0]).to.equals("tag_1"); + expect(result[1]).to.equals("tag_2"); + model.add({ id: 1 }); + expect(model.count()).to.equals(1); + result = model.finalize(); + expect(result[0]).to.equals("tag_2"); + }); +}); + +describe("utilities.js/UrlTracker", () => { + it("Test url tracker", () => { + let urlTracker = new UrlTracker("url_initial"); + let url = urlTracker.getUrl(); + expect(url).to.equals("url_initial"); + expect(urlTracker.atRoot()).to.equals(true); + url = urlTracker.getUrl("url_1"); + expect(url).to.equals("url_1"); + expect(urlTracker.atRoot()).to.equals(false); + url = urlTracker.getUrl("url_2"); + expect(url).to.equals("url_2"); + expect(urlTracker.atRoot()).to.equals(false); + url = urlTracker.getUrl(); + expect(url).to.equals("url_1"); + expect(urlTracker.atRoot()).to.equals(false); + url = urlTracker.getUrl(); + expect(url).to.equals("url_initial"); + expect(urlTracker.atRoot()).to.equals(true); + }); +}); + +describe("services/Services:isDataset", () => { + it("Test dataset identifier", () => { + let services = new Services(mockOptions); + expect(services.isDataset({})).to.equals(false); + expect(services.isDataset({ history_content_type: "dataset" })).to.equals(true); + expect(services.isDataset({ history_content_type: "xyz" })).to.equals(false); + expect(services.isDataset({ type: "file" })).to.equals(true); + expect(services.getRecord({ hid: 1, history_content_type: "dataset" }).isDataset).to.equals(true); + expect(services.getRecord({ hid: 2, history_content_type: "xyz" }).isDataset).to.equals(false); + expect(services.getRecord({ type: "file" }).isDataset).to.equals(true); + }); +}); + +describe("services.js/Services", () => { + it("Test data population from raw data", () => { + let rawData = { + hid: 1, + id: 1, + history_id: 0, + name: "name_1" + }; + let services = new Services(mockOptions); + let items = services.getItems(rawData); + expect(items.length).to.equals(1); + let first = items[0]; + expect(first.name).to.equals("1: name_1"); + expect(first.download).to.equals("host/api/histories/0/contents/1/display"); + }); +}); + +describe("DataDialog.vue", () => { + let stub; + let wrapper; + let emitted; + + let rawData = [ + { + id: 1, + hid: 1, + name: "dataset_1", + history_content_type: "dataset" + }, + { + id: 2, + name: "dataset_2", + type: "file" + }, + { + id: 3, + hid: 3, + name: "collection_1", + history_content_type: "dataset_collection" + } + ]; + + let mockServices = class { + get(url) { + let services = new Services(mockOptions); + let items = services.getItems(rawData); + return new Promise((resolve, reject) => { + resolve(items); + }); + } + }; + + beforeEach(() => { + rewire.__Rewire__("Services", mockServices); + }); + + afterEach(() => { + if (stub) stub.restore(); + }); + + it("loads correctly, shows alert", () => { + wrapper = mount(DataDialog, { + propsData: mockOptions + }); + emitted = wrapper.emitted(); + expect(wrapper.classes()).contain("data-dialog-modal"); + expect(wrapper.find(".fa-spinner").text()).to.equals(""); + expect(wrapper.find(".btn-secondary").text()).to.equals("Clear"); + expect(wrapper.find(".btn-primary").text()).to.equals("Ok"); + expect(wrapper.contains(".fa-spinner")).to.equals(true); + return Vue.nextTick().then(() => { + expect(wrapper.findAll(".fa-copy").length).to.equals(2); + expect(wrapper.findAll(".fa-file-o").length).to.equals(2); + }); + }); + + it("loads correctly, shows datasets and folders", () => { + wrapper = mount(DataDialog, { + propsData: mockOptions + }); + emitted = wrapper.emitted(); + expect(wrapper.classes()).contain("data-dialog-modal"); + expect(wrapper.find(".fa-spinner").text()).to.equals(""); + expect(wrapper.find(".btn-secondary").text()).to.equals("Clear"); + expect(wrapper.find(".btn-primary").text()).to.equals("Ok"); + expect(wrapper.contains(".fa-spinner")).to.equals(true); + return Vue.nextTick().then(() => { + expect(wrapper.findAll(".fa-copy").length).to.equals(2); + expect(wrapper.findAll(".fa-file-o").length).to.equals(2); + }); + }); +}); diff --git a/client/galaxy/scripts/components/DataDialog/DataDialog.vue b/client/galaxy/scripts/components/DataDialog/DataDialog.vue new file mode 100644 index 000000000000..04abd4d86a26 --- /dev/null +++ b/client/galaxy/scripts/components/DataDialog/DataDialog.vue @@ -0,0 +1,160 @@ + + + + diff --git a/client/galaxy/scripts/components/DataDialog/DataDialogSearch.vue b/client/galaxy/scripts/components/DataDialog/DataDialogSearch.vue new file mode 100644 index 000000000000..fc6a6f4ec07a --- /dev/null +++ b/client/galaxy/scripts/components/DataDialog/DataDialogSearch.vue @@ -0,0 +1,29 @@ + + + diff --git a/client/galaxy/scripts/components/DataDialog/DataDialogTable.vue b/client/galaxy/scripts/components/DataDialog/DataDialogTable.vue new file mode 100644 index 000000000000..c3ad745bdab9 --- /dev/null +++ b/client/galaxy/scripts/components/DataDialog/DataDialogTable.vue @@ -0,0 +1,115 @@ + + + diff --git a/client/galaxy/scripts/components/DataDialog/model.js b/client/galaxy/scripts/components/DataDialog/model.js new file mode 100644 index 000000000000..072445a0f5fa --- /dev/null +++ b/client/galaxy/scripts/components/DataDialog/model.js @@ -0,0 +1,52 @@ +export class Model { + constructor(options = {}) { + this.values = {}; + this.multiple = options.multiple || false; + this.format = options.format || null; + } + + /** Adds a new record to the value stack **/ + add(record) { + if (!this.multiple) { + this.values = {}; + } + let key = record && record.id; + if (key) { + if (!this.values[key]) { + this.values[key] = record; + } else { + delete this.values[key]; + } + } else { + throw "Invalid record with no ."; + } + } + + /** Returns the number of added records **/ + count() { + return Object.keys(this.values).length; + } + + /** Returns true if a record is available for a given key **/ + exists(key) { + return !!this.values[key]; + } + + /** Finalizes the results from added records **/ + finalize() { + let results = []; + Object.values(this.values).forEach(v => { + let value = null; + if (this.format) { + value = v[this.format]; + } else { + value = v; + } + results.push(value); + }); + if (results.length > 0 && !this.multiple) { + results = results[0]; + } + return results; + } +} diff --git a/client/galaxy/scripts/components/DataDialog/services.js b/client/galaxy/scripts/components/DataDialog/services.js new file mode 100644 index 000000000000..debf80724c7a --- /dev/null +++ b/client/galaxy/scripts/components/DataDialog/services.js @@ -0,0 +1,82 @@ +import axios from "axios"; + +/** Data populator traverses raw server responses **/ +export class Services { + constructor(options = {}) { + this.root = options.root; + this.host = options.host; + } + + get(url) { + return new Promise((resolve, reject) => { + axios + .get(url) + .then(response => { + let items = this.getItems(response.data); + resolve(items); + }) + .catch(e => { + let errorMessage = "Request failed."; + if (e.response) { + errorMessage = e.response.data.err_msg || `${e.response.statusText} (${e.response.status})`; + } + reject(errorMessage); + }); + }); + } + + /** Returns the formatted results **/ + getItems(data) { + let items = []; + let stack = [data]; + while (stack.length > 0) { + let root = stack.pop(); + if (Array.isArray(root)) { + root.forEach(element => { + stack.push(element); + }); + } else if (root.elements) { + stack.push(root.elements); + } else if (root.object) { + stack.push(root.object); + } else { + let record = this.getRecord(root); + if (record) { + items.push(record); + } + } + } + return items; + } + + /** Populate record data from raw record source **/ + getRecord(record) { + record.details = record.extension || record.description; + record.time = record.update_time || record.create_time; + record.isDataset = this.isDataset(record); + if (record.time) { + record.time = record.time.substring(0, 16).replace("T", " "); + } + if (record.model_class == "Library") { + record.url = `${this.root}api/libraries/${record.id}/contents`; + return record; + } else if (record.hid) { + record.name = `${record.hid}: ${record.name}`; + record.download = `${this.host}/api/histories/${record.history_id}/contents/${record.id}/display`; + return record; + } else if (record.type == "file") { + if (record.name && record.name[0] === "/") { + record.name = record.name.substring(1); + } + record.download = `${this.host}${this.root}api/libraries/datasets/download/uncompressed?ld_ids=${ + record.id + }`; + return record; + } + } + + /** Checks if record is a dataset or drillable **/ + isDataset(record) { + return record.history_content_type == "dataset" || record.type == "file"; + } +} diff --git a/client/galaxy/scripts/components/DataDialog/utilities.js b/client/galaxy/scripts/components/DataDialog/utilities.js new file mode 100644 index 000000000000..8ebcd1c3132c --- /dev/null +++ b/client/galaxy/scripts/components/DataDialog/utilities.js @@ -0,0 +1,28 @@ +/** This helps track urls for data drilling **/ +export class UrlTracker { + constructor(root) { + this.root = root; + this.navigation = []; + } + + /** Returns urls for data drilling **/ + getUrl(url) { + if (url) { + this.navigation.push(url); + } else { + this.navigation.pop(); + let navigationLength = this.navigation.length; + if (navigationLength > 0) { + url = this.navigation[navigationLength - 1]; + } else { + url = this.root; + } + } + return url; + } + + /** Returns true if the last data is at navigation root **/ + atRoot() { + return this.navigation.length == 0; + } +} diff --git a/client/galaxy/scripts/layout/data.js b/client/galaxy/scripts/layout/data.js index e62980954907..44f4cad5b6eb 100644 --- a/client/galaxy/scripts/layout/data.js +++ b/client/galaxy/scripts/layout/data.js @@ -1,5 +1,5 @@ import $ from "jquery"; -import DataDialog from "components/DataDialog.vue"; +import DataDialog from "components/DataDialog/DataDialog.vue"; import Vue from "vue"; import { getGalaxyInstance } from "app"; import { getAppRoot } from "onload/loadConfig"; @@ -10,7 +10,14 @@ export default class Data { * @param {function} callback - Result function called with selection */ dialog(callback, options = {}) { - options.callback = callback; + let galaxy = getGalaxyInstance(); + let host = `${window.location.protocol}//${window.location.hostname}:${window.location.port}`; + Object.assign(options, { + callback: callback, + history: galaxy.currHistoryPanel && galaxy.currHistoryPanel.model.id, + root: galaxy.root, + host: host + }); var instance = Vue.extend(DataDialog); var vm = document.createElement("div"); $("body").append(vm); diff --git a/client/galaxy/scripts/mvc/ui/ui-options.js b/client/galaxy/scripts/mvc/ui/ui-options.js index 7b7ab975cba6..03fe661287a1 100644 --- a/client/galaxy/scripts/mvc/ui/ui-options.js +++ b/client/galaxy/scripts/mvc/ui/ui-options.js @@ -12,6 +12,7 @@ var Base = Backbone.View.extend({ (options && options.model) || new Backbone.Model({ visible: true, + cls: null, data: [], id: Utils.uid(), error_text: "No options available.", @@ -36,6 +37,7 @@ var Base = Backbone.View.extend({ .empty() .removeClass() .addClass("ui-options") + .addClass(this.model.get("cls")) .append((this.$message = $("
").addClass("mt-2"))) .append((this.$menu = $("
").addClass("ui-options-menu"))) .append((this.$options = $(this._template()))); @@ -154,6 +156,16 @@ var Base = Backbone.View.extend({ return this.$(".ui-option").length; }, + /** Shows the options */ + show: function() { + this.model.set("visible", true); + }, + + /** Hides the options */ + hide: function() { + this.model.set("visible", false); + }, + /** Set value to dom */ _setValue: function(new_value) { var self = this; @@ -260,7 +272,7 @@ RadioButton.View = Base.extend({ /** Template for a single option */ _templateOption: function(pair) { - var $el = $("