Merge branch 'dev' into list_icons

This commit is contained in:
guerler
2020-02-09 12:12:32 -05:00
308 changed files with 6403 additions and 5666 deletions
+2 -1
View File
@@ -35,10 +35,11 @@ jobs:
- uses: actions/checkout@v1
with:
fetch-depth: 1
path: 'galaxy root'
- uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Cache venv dir
- name: Cache pip dir
uses: actions/cache@v1
id: pip-cache
with:
+75 -66
View File
@@ -2,10 +2,7 @@
Galaxy welcomes new development! This document briefly describes how to
contribute to the [core galaxy
project](https://github.com/galaxyproject/galaxy) - also checkout our 2013
Galaxy Community Conference presentation on the topic
([video](https://vimeo.com/channels/581875/73486255),
[presentation](https://depot.galaxyproject.org/hub/attachments/documents/presentations/gcc2013/BakerContribute.pdf)).
repository](https://github.com/galaxyproject/galaxy).
For information on contributing more broadly to the Galaxy ecosystem and a
deeper discussion of some of these points - please see the
[Develop](https://galaxyproject.org/develop) section of the [Galaxy Community
@@ -18,12 +15,11 @@ best to communicate with Galaxy developers early. The primary venue for this is
the [GitHub issue tracker](https://github.com/galaxyproject/galaxy/issues).
Browse through existing GitHub issues and if one seems related, comment on it.
For more direct communication, Galaxy developers are generally available via
[IRC](https://galaxyproject.org/get-involved),
[Gitter](https://gitter.im/galaxyproject/Lobby) and on the [development mailing
[Gitter](https://gitter.im/galaxyproject/dev) and on the [development mailing
list](http://dev.list.galaxyproject.org).
If you're looking to help but aren't sure where to start, we also maintain a
[tag](https://github.com/galaxyproject/galaxy/issues?q=is%3Aissue+is%3Aopen+label%3Afriendliness%2Ffriendly)
[tag](https://github.com/galaxyproject/galaxy/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22)
on GitHub for smaller issues we believe would make the best entry points for
new developers.
@@ -34,83 +30,96 @@ If no existing Galaxy issue seems appropriate, a new issue can be opened using
## How to Contribute
* All changes to the [core galaxy
project](https://github.com/galaxyproject/galaxy) should be made through pull
requests to this repository (with just two exceptions outlined below).
All changes to the [core galaxy
repository](https://github.com/galaxyproject/galaxy) should be made through pull
requests (with just two exceptions outlined below).
* If you are new to Git, the [Try Git](http://try.github.com/) tutorial is a
good places to start. More learning resources are listed at
https://help.github.com/articles/good-resources-for-learning-git-and-github/
If you are new to Git, the Software Carpentry's [Version Control with
Git](https://swcarpentry.github.io/git-novice/) tutorial is a good place to
start. More learning resources are listed at
https://help.github.com/en/github/getting-started-with-github/git-and-github-learning-resources
* Make sure you have a free [GitHub](https://github.com/) account.
1. Make sure you have a free [GitHub](https://github.com/) account.
* Fork the [galaxy repository](https://github.com/galaxyproject/galaxy) on
GitHub to make your changes. To keep your copy up to date with respect to
the main repository, you need to frequently [sync your
fork](https://help.github.com/articles/syncing-a-fork/):
```
$ git remote add upstream https://github.com/galaxyproject/galaxy
$ git fetch upstream
$ git checkout dev
$ git merge upstream/dev
```
2. Fork the [galaxy repository](https://github.com/galaxyproject/galaxy) on
GitHub to make your changes. To keep your copy up to date with respect to
the main repository, you need to frequently [sync your
fork](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/syncing-a-fork):
* Choose the correct branch to develop your changes against.
```
$ git remote add upstream https://github.com/galaxyproject/galaxy
$ git fetch upstream
$ git checkout dev
$ git merge upstream/dev
```
* Additions of new features to the code base should be pushed to the `dev`
branch (`git checkout dev`).
3. Choose the correct branch to develop your changes against.
* Most bug fixes to previously release components (things in galaxy-dist)
should be made against the recent `release_XX.XX` branch (`git checkout
release_XX.XX`).
* Additions of new features to the codebase should be pushed to the `dev`
branch (`git checkout dev`).
* Serious security problems should not be fixed via pull request - please see
[the Galaxy security policies](SECURITY_POLICY.md) for information about
responsibly disclosing security issues.
* Most bug fixes to previous (or frozen) releases should be made against the
corrisponding `release_XX.XX` branch (`git checkout release_XX.XX`).
* If your changes modify code please ensure the resulting files conform to
Galaxy [style guidelines](https://galaxyproject.org/develop/best-practices).
* Serious security problems should not be fixed via pull request - please see
[the Galaxy security policies](SECURITY_POLICY.md) for information about
responsibly disclosing security issues.
* If you are working on the Galaxy Client (the user interface -- JavaScript,
styles, etc.), see more information in the [client readme](client/README.md).
4. If your changes modify code please ensure the resulting files conform to
the [style guidelines](#style-guidelines) below.
* Galaxy contains hundreds of tests of different types and complexity and
running each is difficult and probably not reasonable at this time (someday
we will provide a holistic test procedure to make this possible). For now,
please just review the [running tests
documentation](https://galaxyproject.org/admin/running-tests) and run any
that seem relevant. Developers reviewing your pull request will be happy to
help guide you to running the most relevant tests as part of the pull request
review process and may request the output of these tests. You can run the
continuous integration tests locally using `tox`, example: `tox -e
py27-lint,py27-unit`.
If you are working on the Galaxy user interface (i.e. JavaScript,
styles, etc.), see more information in the [client README](client/README.md).
* Commit and push your changes to your
[fork](https://help.github.com/articles/pushing-to-a-remote/).
5. Galaxy contains hundreds of tests of different types and complexity and
running each is difficult and probably not reasonable on your workstation. So
please review the [running tests documentation](test/TESTING.md) and run any
that seem relevant.
* Open a [pull
request](https://help.github.com/articles/creating-a-pull-request/) with
these changes. You pull request message ideally should include:
If possible, also try to add new tests for the features added or bugs fixed
by your pull request.
* A description of why the changes should be made.
Developers reviewing your pull request will be happy to help you add or run
the relevant tests as part of the pull request review process.
6. Commit and push your changes to your
[fork](https://help.github.com/en/github/using-git/pushing-commits-to-a-remote-repository).
7. Open a [pull
request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request)
with these changes. Your pull request message ideally should include:
* Why you made the changes (e.g. references to GitHub issues being fixed).
* A description of the implementation of the changes.
* A description of how to test the changes.
* How to test the changes, if you haven't included specific tests already.
* The pull request should pass all the continuous integration tests which are
automatically run by GitHub using e.g. Travis CI.
8. The pull request should pass all the continuous integration tests which are
automatically started by GitHub using e.g. Travis CI.
* Your pull request will be handled according to [some
rules](doc/source/project/organization.rst#handling-pull-requests).
9. Your pull request will be handled according to [some
rules](doc/source/project/organization.rst#handling-pull-requests).
## Style guidelines
### Python
- Galaxy follows [PEP-8](https://www.python.org/dev/peps/pep-0008/), with
particular emphasis on readability being the ultimate goal:
- 4 spaces (not tabs!) per indentation level
- divergences from PEP-8 are listed in the `[flake8]` section of the `setup.cfg`
file.
- Python imports should be ordered following the
[smarkets](https://github.com/PyCQA/flake8-import-order/blob/master/tests/test_cases/complete_smarkets.py)
style.
- Python [docstrings](http://www.python.org/dev/peps/pep-0257/) need to be in
[reStructured Text (RST)](https://docutils.sourceforge.io/rst.html) format and
compatible with [Sphinx](https://www.sphinx-doc.org).
## A Quick Note about Tools
For the most part, Galaxy tools should be published to the [Tool
Shed](https://galaxyproject.org/toolshed) and not in this repository
directly. If you are looking for migrated core tools that used to exist in
this repository - please checkout the
[tools-iuc](https://github.com/galaxyproject/tools-iuc) repository on GitHub.
More information about tool development can be found [on the community
hub](https://galaxyproject.org/develop).
For the most part, Galaxy tools should be published to a [Tool
Shed](https://galaxyproject.org/toolshed) and not in this repository directly.
More information about tool development can be found [on the community
hub](https://galaxyproject.org/develop).
+20 -8
View File
@@ -1,5 +1,9 @@
module.exports = {
extends: ["eslint:recommended", "plugin:vue/essential"], // airbnb-base, eventually
extends: [
"eslint:recommended",
"plugin:vue/strongly-recommended",
//"airbnb-base", eventually
],
env: {
browser: true,
commonjs: true,
@@ -13,16 +17,24 @@ module.exports = {
},
plugins: ["html"],
rules: {
// Standard rules
"no-console": "off",
"no-unused-vars": ["error", { args: "none" }],
"prefer-const": "error",
// I'd love to turn on camelcase, but it's a big shift with tons of current errors.
// camelcase: [
// "error",
// {
// properties: "always"
// }
// ]
// Vue TODO (enable these)
"vue/require-default-prop": "off",
"vue/require-prop-types": "off",
"vue/prop-name-casing": "off",
// Prettier compromises/workarounds -- mostly #wontfix?
"vue/html-indent": "off",
"vue/max-attributes-per-line": "off",
"vue/html-self-closing": "off",
"vue/singleline-html-element-content-newline": "off",
"vue/multiline-html-element-content-newline": "off",
"vue/html-closing-bracket-newline": "off",
"vue/html-closing-bracket-spacing": "off"
},
globals: {
// chai tests
+9 -10
View File
@@ -73,10 +73,9 @@ Even Better Automatic Rebuilding (HMR)
======================================
For even more rapid development you can use the webpack development server for
HMR. This technique allows swapping out of modules while the application is
running without requiring a full page reload most of the time. Upon editing
and saving client files webpack will rebuild the necessary modules and then
notify your web to update the page with your new code.
Hot Module Replacement (HMR). This technique allows swapping out of modules
while the application is running without requiring a full page reload most of
the time.
Setting this up is a little more involved, but it is the fastest possible way
to iterate when developing the client. You'll need to start two separate
@@ -151,26 +150,26 @@ directory. This is what happens during a complete client build.
During client-side development, it is more convenient to have granular testing
options. The various testing scripts are defined inside package.json within the
client folder, and are called either with npm (or yarn) as follows:
client folder, and are called either with `yarn` as demonstrated in the
following commands.
#### Run all tests (mocha and qunit):
npm run test
yarn run test
#### Watch and rerun all client-side unit-tests every time a source file changes:
npm run test-watch
yarn run test-watch
#### Run only specified test files when a source file changes:
npm run test-watch watch-only="Tags/*.test.js"
yarn run test-watch watch-only="Tags/*.test.js"
(The above watch-only parameter will be mapped to the following glob expression "**/Tags/*.test.js")
#### Run qunit legacy tests
npm run test-qunit
yarn run test-qunit
## Writing a test file
+1 -1
View File
@@ -7,7 +7,7 @@ import metricsLogger from "utils/metrics-logger";
import addLogging from "utils/add-logging";
import localize from "utils/localization";
import { getGalaxyInstance } from "app";
import { create, dialog } from "utils/data.js";
import { create, dialog } from "utils/data";
// ============================================================================
/** Base galaxy client-side application.
@@ -1,8 +1,8 @@
import DataDialog from "./DataDialog.vue";
import SelectionDialog from "components/SelectionDialog/SelectionDialog.vue";
import { __RewireAPI__ as rewire } from "./DataDialog";
import { Model } from "./model.js";
import { UrlTracker } from "./utilities.js";
import { Model } from "./model";
import { UrlTracker } from "./utilities";
import { Services } from "./services";
import { mount, createLocalVue } from "@vue/test-utils";
@@ -1,9 +1,9 @@
<template>
<selection-dialog
:errorMessage="errorMessage"
:optionsShow="optionsShow"
:modalShow="modalShow"
:hideModal="() => (modalShow = false)"
:error-message="errorMessage"
:options-show="optionsShow"
:modal-show="modalShow"
:hide-modal="() => (modalShow = false)"
>
<template v-slot:search>
<data-dialog-search v-model="filter" />
@@ -41,9 +41,9 @@
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import SelectionDialogMixin from "components/SelectionDialog/SelectionDialogMixin";
import { UrlTracker } from "./utilities.js";
import { Model } from "./model.js";
import { Services } from "./services.js";
import { UrlTracker } from "./utilities";
import { Model } from "./model";
import { Services } from "./services";
Vue.use(BootstrapVue);
@@ -32,7 +32,7 @@
<script>
import { getAppRoot } from "onload/loadConfig";
import { getGalaxyInstance } from "app";
import { Services } from "./services.js";
import { Services } from "./services";
import DatasetName from "./DatasetName";
import DatasetHistory from "./DatasetHistory";
import DelayedInput from "components/Common/DelayedInput";
@@ -7,11 +7,11 @@
<button id="import" class="btn btn-secondary">Import and start using history</button>
</span>
<span v-if="historyData.user_is_owner && historyData.history_is_current == false">
<button id="switch-history" class="btn btn-secondary" v-on:click="switchHistory">
<button id="switch-history" class="btn btn-secondary" @click="switchHistory">
Switch to this history
</button>
</span>
<button id="show-structure" class="btn btn-secondary" v-on:click="showStructure">
<button id="show-structure" class="btn btn-secondary" @click="showStructure">
Show structure
</button>
</span>
@@ -3,6 +3,7 @@ import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { mount, createLocalVue } from "@vue/test-utils";
import { createStore } from "../../store";
import flushPromises from "flush-promises";
import JobMetrics from "./JobMetrics";
const JOB_ID = "moo";
@@ -50,10 +51,8 @@ describe("JobMetrics/JobMetrics.vue", () => {
propsData,
localVue
});
// One tick for axios request.
await wrapper.vm.$nextTick();
// One tick for Vue to render.
await wrapper.vm.$nextTick();
// Wait for axios and rendering.
await flushPromises();
expect(wrapper.vm.jobMetrics.length).to.equals(3);
expect(wrapper.vm.jobId).to.equals(JOB_ID);
expect(wrapper.vm.metricsByPlugins.core.runtime).to.equals(145);
@@ -1,11 +1,11 @@
<template>
<div v-if="orderedPlugins.length > 0">
<h3 v-if="includeTitle">Job Metrics</h3>
<div v-for="plugin in orderedPlugins" v-bind:key="plugin" class="metrics_plugin">
<div v-for="plugin in orderedPlugins" :key="plugin" class="metrics_plugin">
<h4 class="metrics_plugin_title">{{ plugin }}</h4>
<table class="tabletip info_data_table">
<tbody>
<tr v-for="(metricValue, metricTitle) in metricsByPlugins[plugin]" v-bind:key="metricTitle">
<tr v-for="(metricValue, metricTitle) in metricsByPlugins[plugin]" :key="metricTitle">
<td>{{ metricTitle }}</td>
<td>{{ metricValue }}</td>
</tr>
@@ -10,13 +10,13 @@
</tr>
</thead>
<tbody>
<tr v-for="(parameter, index) in parameters" v-bind:key="index">
<td v-bind:style="{ 'padding-left': `${(parameter.depth - 1) * 10}px` }">
<tr v-for="(parameter, pIndex) in parameters" :key="pIndex">
<td :style="{ 'padding-left': `${(parameter.depth - 1) * 10}px` }">
{{ parameter.text }}
</td>
<td v-if="Array.isArray(parameter.value)">
<ul style="padding-inline-start: 25px;">
<li v-for="(elVal, index) in parameter.value" v-bind:key="index">
<li v-for="(elVal, pvIndex) in parameter.value" :key="pvIndex">
<span v-if="elVal.src == 'hda'">
<a :href="appRoot() + 'datasets/' + elVal.id + '/show_params'">
{{ elVal.hid }}: {{ elVal.name }}
@@ -3,10 +3,10 @@
{{ simpleDescription }}
</div>
<div v-else-if="!jobStatesSummary || !jobStatesSummary.hasDetails()">
<progress-bar :note="loadingNote" :loading="true" :infoProgress="1" />
<progress-bar :note="loadingNote" :loading="true" :info-progress="1" />
</div>
<div v-else-if="isNew">
<progress-bar note="Creating jobs" :loading="true" :infoProgress="1" />
<progress-bar note="Creating jobs" :loading="true" :info-progress="1" />
</div>
<div v-else-if="isErrored">
{{ errorDescription }}
@@ -14,9 +14,9 @@
<div v-else>
<progress-bar
:note="generatingNote"
:okProgress="okPercent"
:runningProgress="runningPercent"
:newProgress="otherPercent"
:ok-progress="okPercent"
:running-progress="runningPercent"
:new-progress="otherPercent"
/>
</div>
</template>
@@ -1,6 +1,6 @@
<template>
<span>
<span v-bind:class="spinnerClasses"></span>
<span :class="spinnerClasses"></span>
<span class="loading-message">{{ message }}.<span class="blinking">..</span></span>
</span>
</template>
@@ -1,5 +1,5 @@
<template>
<markdown :markdown-config="markdownConfig" :exportLink="exportUrl"> </markdown>
<markdown :markdown-config="markdownConfig" :export-link="exportUrl"> </markdown>
</template>
<script>
@@ -5,7 +5,7 @@
:onupdate="onUpdate"
ref="markdownEditor"
:toolbar="true"
:showMarkdownHelp="showMarkdownHelp"
:show-markdown-help="showMarkdownHelp"
>
</markdown-editor>
</div>
@@ -55,9 +55,9 @@ export default {
created() {
const Galaxy = getGalaxyInstance();
Galaxy.upload.ui_button.model.on("change", () => {
this.status = Galaxy.upload.ui_button.model.attributes.status;
this.percentage = Galaxy.upload.ui_button.model.attributes.percentage;
Galaxy.upload.model.on("change", () => {
this.status = Galaxy.upload.model.attributes.status;
this.percentage = Galaxy.upload.model.attributes.percentage;
});
}
};
@@ -47,7 +47,7 @@ describe("ToolSection", () => {
expect(wrapper.findAll(".name").length).to.equal(1);
});
it("test tool slider state", () => {
it("test tool slider state", async () => {
const wrapper = mount(ToolSection, {
propsData: {
category: {
@@ -65,20 +65,34 @@ describe("ToolSection", () => {
}
});
expect(wrapper.vm.opened).to.equal(true);
const $sectionName = wrapper.find(".name");
$sectionName.trigger("click");
await Vue.nextTick();
expect(wrapper.vm.opened).to.equal(false);
wrapper.vm.queryFilter = "";
await Vue.nextTick();
expect(wrapper.vm.opened).to.equal(false);
wrapper.vm.queryFilter = "test";
await Vue.nextTick();
expect(wrapper.vm.opened).to.equal(true);
wrapper.vm.disableFilter = true;
await Vue.nextTick();
expect(wrapper.vm.opened).to.equal(true);
wrapper.vm.queryFilter = "";
await Vue.nextTick();
expect(wrapper.vm.opened).to.equal(false);
$sectionName.trigger("click");
await Vue.nextTick();
expect(wrapper.vm.opened).to.equal(true);
wrapper.vm.queryFilter = "test";
await Vue.nextTick();
expect(wrapper.vm.opened).to.equal(false);
});
});
@@ -20,10 +20,10 @@
class="ml-2"
:tool="el"
:key="key"
:toolKey="toolKey"
:hideName="hideName"
:operationTitle="operationTitle"
:operationIcon="operationIcon"
:tool-key="toolKey"
:hide-name="hideName"
:operation-title="operationTitle"
:operation-icon="operationIcon"
@onOperation="onOperation"
@onClick="onClick"
/>
@@ -39,9 +39,9 @@
<tool
v-else
:tool="category"
:hideName="hideName"
:operationTitle="operationTitle"
:operationIcon="operationIcon"
:hide-name="hideName"
:operation-title="operationTitle"
:operation-icon="operationIcon"
@onOperation="onOperation"
@onClick="onClick"
/>
@@ -18,7 +18,7 @@
<tool-section
v-for="category in categories"
:category="category"
:queryFilter="query"
:query-filter="query"
:key="category.id"
@onClick="onOpen"
/>
@@ -28,9 +28,9 @@
</div>
<div id="internal-workflows" class="toolSectionBody">
<div class="toolSectionBg" />
<div class="toolTitle" v-for="workflow in this.workflows" :key="workflow.id">
<a :href="workflow.href">
{{ workflow.title }}
<div class="toolTitle" v-for="wf in this.workflows" :key="wf.id">
<a :href="wf.href">
{{ wf.title }}
</a>
</div>
</div>
@@ -44,7 +44,7 @@ import ToolSection from "./Common/ToolSection";
import ToolSearch from "./Common/ToolSearch";
import UploadButton from "./Buttons/UploadButton";
import FavoritesButton from "./Buttons/FavoritesButton";
import { filterToolSections } from "./utilities.js";
import { filterToolSections } from "./utilities";
import { getGalaxyInstance } from "app";
import { getAppRoot } from "onload";
import _l from "utils/localization";
@@ -12,27 +12,27 @@
<div class="toolMenuContainer">
<tool-section
v-for="category in moduleSections"
:hideName="true"
:hide-name="true"
:category="category"
toolKey="name"
:sectionName="category.name"
:queryFilter="query"
:disableFilter="true"
tool-key="name"
:section-name="category.name"
:query-filter="query"
:disable-filter="true"
:key="category.name"
@onClick="onInsertModule"
/>
<tool-section
:category="dataManagers"
:key="dataManagers.id"
:queryFilter="query"
:disableFilter="true"
:query-filter="query"
:disable-filter="true"
@onClick="onInsertTool"
/>
<div class="toolMenu" id="workflow-tool-menu">
<tool-section
v-for="category in categories"
:category="category"
:queryFilter="query"
:query-filter="query"
:key="category.id"
@onClick="onInsertTool"
/>
@@ -40,10 +40,10 @@
<tool-section
:category="workflowSection"
:key="workflowSection.name"
operationIcon="fa fa-copy"
operationTitle="Insert individual steps."
:queryFilter="query"
:disableFilter="true"
operation-icon="fa fa-copy"
operation-title="Insert individual steps."
:query-filter="query"
:disable-filter="true"
@onClick="onInsertWorkflow"
@onOperation="onInsertWorkflowSteps"
/>
@@ -55,7 +55,7 @@
<script>
import ToolSection from "./Common/ToolSection";
import ToolSearch from "./Common/ToolSearch";
import { filterToolSections } from "./utilities.js";
import { filterToolSections } from "./utilities";
export default {
name: "ToolBox",
@@ -3,15 +3,11 @@
<span class="note" v-if="note">
{{ note }}<span v-if="loading">.<span class="blinking">..</span> </span>
</span>
<div class="progress-bar info" v-bind:style="styleFromProgress(infoProgress)" :title="infoMessage"></div>
<div class="progress-bar ok" v-bind:style="styleFromProgress(okProgress)" :title="okMessage"></div>
<div
class="progress-bar running"
v-bind:style="styleFromProgress(runningProgress)"
:title="runningMessage"
></div>
<div class="progress-bar new" v-bind:style="styleFromProgress(newProgress)" :title="newMessage"></div>
<div class="progress-bar error" v-bind:style="styleFromProgress(errorProgress)" :title="errorMessage"></div>
<div class="progress-bar info" :style="styleFromProgress(infoProgress)" :title="infoMessage"></div>
<div class="progress-bar ok" :style="styleFromProgress(okProgress)" :title="okMessage"></div>
<div class="progress-bar running" :style="styleFromProgress(runningProgress)" :title="runningMessage"></div>
<div class="progress-bar new" :style="styleFromProgress(newProgress)" :title="newMessage"></div>
<div class="progress-bar error" :style="styleFromProgress(errorProgress)" :title="errorMessage"></div>
</div>
</template>
<script>
@@ -30,14 +30,14 @@
<!-- column-headers -->
<div
class="rule-builder-body vertically-spaced"
v-bind:class="{ 'flex-column-container': vertical }"
:class="{ 'flex-column-container': vertical }"
v-if="ruleView == 'normal'"
>
<!-- width: 30%; -->
<div class="rule-column" v-bind:class="orientation">
<div class="rule-column" :class="orientation">
<div
class="rules-container"
v-bind:class="{
:class="{
'rules-container-vertical': initialElements && vertical,
'rules-container-horizontal': initialElements && horizontal,
'rules-container-full': initialElements == null
@@ -267,7 +267,7 @@
</label>
</rule-component>
<div v-if="displayRuleType == 'mapping'">
<div class="map" v-for="(map, index) in mapping" v-bind:index="index" v-bind:key="map.type">
<div class="map" v-for="(map, index) in mapping" :index="index" :key="map.type">
<column-selector
:class="'rule-map-' + map.type.replace(/_/g, '-')"
:label="mappingTargets()[map.type].label"
@@ -300,8 +300,8 @@
<div class="dropdown-menu" role="menu">
<a
v-for="target in unmappedTargets"
v-bind:index="target"
v-bind:key="target"
:index="target"
:key="target"
class="dropdown-item"
href="javascript:void(0)"
:class="'rule-add-mapping-' + target.replace(/_/g, '-')"
@@ -337,9 +337,9 @@
<!-- Example at the end of https://vuejs.org/v2/guide/list.html -->
<rule-display
v-for="(rule, index) in rules"
v-bind:rule="rule"
v-bind:index="index"
v-bind:key="index"
:rule="rule"
:index="index"
:key="index"
@edit="editRule(rule, index)"
@remove="removeRule(index)"
:col-headers="colHeadersPerRule[index]"
@@ -347,12 +347,12 @@
<identifier-display
v-for="(map, index) in mapping"
v-bind="map"
v-bind:index="index"
v-bind:key="map.type"
:index="index"
:key="map.type"
@remove="removeMapping(index)"
@edit="displayRuleType = 'mapping'"
v-on:mouseover.native="map.columns.forEach(col => highlightColumn(col))"
v-on:mouseout.native="map.columns.forEach(col => unhighlightColumn(col))"
@mouseover.native="map.columns.forEach(col => highlightColumn(col))"
@mouseout.native="map.columns.forEach(col => unhighlightColumn(col))"
:col-headers="colHeaders"
/>
<div v-if="mapping.length == 0">
@@ -439,19 +439,14 @@
</div>
<!-- flex-column column -->
<!-- style="width: 70%;" -->
<div
class="table-column"
v-bind:class="orientation"
style="width: 100%;"
v-if="initialElements !== null"
>
<div class="table-column" :class="orientation" style="width: 100%;" v-if="initialElements !== null">
<hot-table
id="hot-table"
ref="hotTable"
:data="hotData.data"
:colHeaders="colHeadersDisplay"
:readOnly="true"
stretchH="all"
:col-headers="colHeadersDisplay"
:read-only="true"
stretch-h="all"
>
</hot-table>
</div>
@@ -3,16 +3,16 @@
<ol class="rules">
<rule-display-preview
v-for="(rule, index) in rules"
v-bind:rule="rule"
v-bind:index="index"
v-bind:key="index"
:rule="rule"
:index="index"
:key="index"
:col-headers="columnData.colHeadersPerRule[index]"
/>
<identifier-display-preview
v-for="(map, index) in mapping"
v-bind="map"
v-bind:index="index"
v-bind:key="map.type"
:index="index"
:key="map.type"
:col-headers="colHeaders"
/>
</ol>
+20 -2
View File
@@ -3,13 +3,28 @@
import $ from "jquery";
export default {
props: ["options", "value", "placeholder"],
props: ["options", "value", "placeholder", "containerClass", "enabled"],
template: `<select><slot></slot></select>`,
mounted: function() {
const vm = this;
// TODO: refactor property list to objects that allow defaults and types
let enabled = this.enabled;
if (enabled === undefined) {
enabled = true;
}
const select2Options = {
data: this.options,
placeholder: this.placeholder,
allowClear: !!this.placeholder,
enable: enabled,
dropdownAutoWidth: true
};
if (this.containerClass) {
select2Options.containerCssClass = this.containerClass;
}
$(this.$el)
// init select2
.select2({ data: this.options, placeholder: this.placeholder, allowClear: this.placeholder })
.select2(select2Options)
.val(this.value)
.trigger("change")
// emit event on change.
@@ -27,6 +42,9 @@ export default {
$(this.$el)
.empty()
.select2({ data: options });
},
enabled: function(value) {
$(this.$el).select2("enable", value);
}
},
destroyed: function() {
@@ -1,7 +1,7 @@
import MockAdapter from "axios-mock-adapter";
import axios from "axios";
import DatasetCollectionDialog from "./DatasetCollectionDialog.vue";
import { __RewireAPI__ as rewire } from "./DatasetCollectionDialog";
import { setupTestGalaxy } from "qunit/test-app";
import SelectionDialog from "./SelectionDialog.vue";
import { shallowMount, createLocalVue } from "@vue/test-utils";
@@ -12,10 +12,6 @@ const mockOptions = {
history: "f2db41e1fa331b3e"
};
const galaxyInstance = {
root: "/"
};
describe("DatasetCollectionDialog.vue", () => {
let wrapper;
let localVue;
@@ -23,7 +19,7 @@ describe("DatasetCollectionDialog.vue", () => {
beforeEach(() => {
axiosMock = new MockAdapter(axios);
rewire.__Rewire__("getGalaxyInstance", () => galaxyInstance);
setupTestGalaxy();
localVue = createLocalVue();
wrapper = shallowMount(DatasetCollectionDialog, {
propsData: mockOptions,
@@ -1,9 +1,9 @@
<template>
<selection-dialog
:errorMessage="errorMessage"
:optionsShow="optionsShow"
:modalShow="modalShow"
:hideModal="() => (modalShow = false)"
:error-message="errorMessage"
:options-show="optionsShow"
:modal-show="modalShow"
:hide-modal="() => (modalShow = false)"
>
<template v-slot:search>
<data-dialog-search v-model="filter" />
@@ -14,7 +14,7 @@
:items="items"
:multiple="false"
:filter="filter"
leafIcon="fa fa-folder"
leaf-icon="fa fa-folder"
@clicked="clicked"
@load="load"
/>
@@ -3,6 +3,7 @@ import axios from "axios";
import WorkflowDialog from "./WorkflowDialog.vue";
import { __RewireAPI__ as rewire } from "components/Workflow/services";
import SelectionDialog from "./SelectionDialog.vue";
import { setupTestGalaxy } from "qunit/test-app";
import { shallowMount, createLocalVue } from "@vue/test-utils";
@@ -11,15 +12,6 @@ const mockOptions = {
modalStatic: true
};
const userModel = {
get: () => "username"
};
const galaxyInstance = {
root: "/",
user: userModel
};
describe("WorkflowDialog.vue", () => {
let wrapper;
let localVue;
@@ -27,7 +19,7 @@ describe("WorkflowDialog.vue", () => {
beforeEach(() => {
axiosMock = new MockAdapter(axios);
rewire.__Rewire__("getGalaxyInstance", () => galaxyInstance);
setupTestGalaxy();
rewire.__Rewire__("_addAttributes", workflow => workflow);
localVue = createLocalVue();
wrapper = shallowMount(WorkflowDialog, {
@@ -1,9 +1,9 @@
<template>
<selection-dialog
:errorMessage="errorMessage"
:optionsShow="optionsShow"
:modalShow="modalShow"
:hideModal="() => (modalShow = false)"
:error-message="errorMessage"
:options-show="optionsShow"
:modal-show="modalShow"
:hide-modal="() => (modalShow = false)"
>
<template v-slot:search>
<data-dialog-search v-model="filter" />
@@ -14,7 +14,7 @@
:items="items"
:multiple="false"
:filter="filter"
leafIcon="fa fa-sitemap fa-rotate-270"
leaf-icon="fa fa-sitemap fa-rotate-270"
@clicked="clicked"
@load="load"
/>
@@ -15,7 +15,7 @@
<div v-else>
There are multiple InteractiveTool result views available:
<ul>
<li v-for="entryPoint of entryPoints" v-bind:key="entryPoint.id">
<li v-for="entryPoint of entryPoints" :key="entryPoint.id">
{{ entryPoint.name }}
<span v-if="entryPoint.active"> (<a :href="entryPoints[0].target">click here to display</a>) </span>
<span v-else>
@@ -47,7 +47,10 @@ export default {
total: 0,
error: null,
tabValue: true,
tabOptions: [{ text: "Search All", value: true }, { text: "Installed Only", value: false }]
tabOptions: [
{ text: "Search All", value: true },
{ text: "Installed Only", value: false }
]
};
},
watch: {
@@ -8,14 +8,14 @@
</b-card>
</span>
<div v-else>
<RepositoryDetails :repo="toolshedRepository" :toolshedUrl="repo.tool_shed_url" />
<RepositoryDetails :repo="toolshedRepository" :toolshed-url="repo.tool_shed_url" />
</div>
</div>
</div>
</template>
<script>
import { getAppRoot } from "onload/loadConfig";
import { Services } from "../services.js";
import { Services } from "../services";
import RepositoryDetails from "../RepositoryDetails/Index.vue";
import LoadingSpan from "components/LoadingSpan";
@@ -25,7 +25,7 @@
id="repository-table"
striped
:fields="fields"
:sortBy="sortBy"
:sort-by="sortBy"
:items="repositories"
:filter="filter"
@filtered="filtered"
@@ -60,7 +60,7 @@
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import { getAppRoot } from "onload/loadConfig";
import { Services } from "../services.js";
import { Services } from "../services";
import LoadingSpan from "components/LoadingSpan";
import Monitor from "./Monitor";
import RepositoryDetails from "./Details";
@@ -33,7 +33,7 @@
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import { getAppRoot } from "onload/loadConfig";
import { Services } from "../services.js";
import { Services } from "../services";
import InstallationButton from "../RepositoryDetails/InstallationButton";
Vue.use(BootstrapVue);
@@ -40,9 +40,9 @@
<InstallationSettings
v-if="showSettings"
:repo="repo"
:toolshedUrl="toolshedUrl"
:changesetRevision="selectedChangeset"
:requiresPanel="selectedRequiresPanel"
:toolshed-url="toolshedUrl"
:changeset-revision="selectedChangeset"
:requires-panel="selectedRequiresPanel"
@hide="onHide"
@ok="onOk"
/>
@@ -54,7 +54,7 @@
<script>
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import { Services } from "../services.js";
import { Services } from "../services";
import InstallationSettings from "./InstallationSettings.vue";
import InstallationButton from "./InstallationButton.vue";
import RepositoryTools from "./RepositoryTools.vue";
@@ -1,3 +1,4 @@
import Vue from "vue";
import { mount } from "@vue/test-utils";
import RepositoryTools from "./RepositoryTools";
@@ -21,7 +22,7 @@ describe("RepositoryTools", () => {
expect($second.text()).to.equal("version");
});
it("test collapsing tool version list in repository details", () => {
it("test collapsing tool version list in repository details", async () => {
const wrapper = mount(RepositoryTools, {
propsData: {
tools: [
@@ -56,6 +57,8 @@ describe("RepositoryTools", () => {
expect($third.find("td:last-child").text()).to.equal("");
const $link = wrapper.find("a");
$link.trigger("click");
await Vue.nextTick();
const $elExpanded = wrapper.findAll("tr");
expect($elExpanded.length).to.equal(4);
const $thirdExpanded = $elExpanded.at(3);
@@ -66,6 +69,8 @@ describe("RepositoryTools", () => {
expect($forthExpanded.find("td:last-child").text()).to.equal("");
const $linkExpanded = wrapper.find("a");
$linkExpanded.trigger("click");
await Vue.nextTick();
const $elCollapsed = wrapper.findAll("tr");
expect($elCollapsed.length).to.equal(3);
const $thirdCollapsed = $elCollapsed.at(2);
@@ -18,7 +18,7 @@
<script>
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import { Services } from "../services.js";
import { Services } from "../services";
import LoadingSpan from "components/LoadingSpan";
Vue.use(BootstrapVue);
@@ -1,8 +1,8 @@
<template>
<div>
<ServerSelection
:toolshedUrl="toolshedUrl"
:toolshedUrls="toolshedUrls"
:toolshed-url="toolshedUrl"
:toolshed-urls="toolshedUrls"
:total="total"
:loading="loading"
@onToolshed="setToolshed"
@@ -12,12 +12,12 @@
<Repositories
:query="query"
:scrolled="scrolled"
:toolshedUrl="toolshedUrl"
:toolshed-url="toolshedUrl"
@onError="setError"
v-if="!queryEmpty"
/>
<Categories
:toolshedUrl="toolshedUrl"
:toolshed-url="toolshedUrl"
:loading="loading"
@onCategory="setQuery"
@onTotal="setTotal"
@@ -49,7 +49,10 @@ export default {
total: 0,
error: null,
tabCurrent: "true",
tabOptions: [{ text: "Search All", value: true }, { text: "Installed Only", value: false }]
tabOptions: [
{ text: "Search All", value: true },
{ text: "Installed Only", value: false }
]
};
},
created() {
@@ -2,6 +2,7 @@ import { mount } from "@vue/test-utils";
import Repositories from "./Repositories";
import { __RewireAPI__ as rewire } from "./Repositories";
import Vue from "vue";
import flushPromises from "flush-promises";
describe("Repositories", () => {
beforeEach(() => {
@@ -36,13 +37,17 @@ describe("Repositories", () => {
toolshedUrl: "toolshedUrl"
}
});
// Test initial state prior to the data fetch tick -- should be loading.
expect(wrapper.find(".loading-message").text()).to.equal("Loading repositories...");
wrapper.vm.pageState = wrapper.vm.COMPLETE;
expect(wrapper.find(".unavailable-message").text()).to.equal("No matching repositories found.");
await Vue.nextTick();
const links = wrapper.findAll("a");
expect(links.length).to.equal(2);
expect(links.at(0).text()).to.equal("name_0");
expect(links.at(1).text()).to.equal("name_1");
// Reset repositories and state to test empty.
wrapper.vm.repositories = [];
wrapper.vm.pageState = 2; // COMPLETE is '2'
await Vue.nextTick();
expect(wrapper.find(".unavailable-message").text()).to.equal("No matching repositories found.");
});
});
@@ -8,7 +8,7 @@
<div>{{ row.item.description }}</div>
</template>
<template v-slot:row-details="row">
<RepositoryDetails :repo="row.item" :toolshedUrl="toolshedUrl" />
<RepositoryDetails :repo="row.item" :toolshed-url="toolshedUrl" />
</template>
</b-table>
<div class="unavailable-message" v-if="noResultsFound">
@@ -20,7 +20,7 @@
<script>
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import { Services } from "../services.js";
import { Services } from "../services";
import LoadingSpan from "components/LoadingSpan";
import RepositoryDetails from "../RepositoryDetails/Index.vue";
@@ -1,4 +1,4 @@
import { Services } from "./Services";
import { Services } from "./services";
describe("Toolshed service helpers", () => {
const incoming = [
@@ -0,0 +1,61 @@
import Collection from "./Collection.vue";
import { mountWithApp } from "./test_helpers";
describe("Collection.vue", () => {
it("loads with correct initial state", async () => {
const { wrapper } = mountWithApp(Collection);
expect(wrapper.vm.counterAnnounce).to.equals(0);
expect(wrapper.vm.showHelper).to.equals(true);
expect(wrapper.vm.extensions[0].id).to.equals("ab1");
expect(wrapper.find("#btn-reset").classes()).to.contain("disabled");
expect(wrapper.find("#btn-start").classes()).to.contain("disabled");
expect(wrapper.find("#btn-stop").classes()).to.contain("disabled");
expect(wrapper.findAll(".ui-limitloader").length).to.equals(0);
});
it("does render FTP is site set", async () => {
const { wrapper } = mountWithApp(Collection);
expect(wrapper.find("#btn-ftp").isVisible()).to.equals(true);
});
it("doesn't render FTP is no site set", async () => {
const { wrapper } = mountWithApp(Collection, {
currentFtp: () => {
return null;
}
});
expect(wrapper.findAll("#btn-ftp").length).to.equals(0);
});
it("resets properly", async () => {
const { wrapper, localVue } = mountWithApp(Collection);
expect(wrapper.vm.showHelper).to.equals(true);
await localVue.nextTick();
wrapper.find("#btn-new").trigger("click");
await localVue.nextTick();
expect(wrapper.vm.showHelper).to.equals(false);
expect(wrapper.vm.counterAnnounce).to.equals(1);
expect(wrapper.find("#btn-reset").classes()).to.not.contain("disabled");
expect(wrapper.find("#btn-start").classes()).to.not.contain("disabled");
wrapper.find("#btn-reset").trigger("click");
await localVue.nextTick();
expect(wrapper.vm.showHelper).to.equals(true);
});
it("respects lazyLoadMax limit", async () => {
const { wrapper, localVue } = mountWithApp(Collection, {}, { lazyLoadMax: 2 });
expect(wrapper.findAll(".ui-limitloader").length).to.equals(1);
await localVue.nextTick();
wrapper.find("#btn-new").trigger("click");
await localVue.nextTick();
wrapper.find("#btn-new").trigger("click");
await localVue.nextTick();
expect(wrapper.findAll("table tbody tr").length).to.equals(2);
wrapper.find("#btn-new").trigger("click");
await localVue.nextTick();
expect(wrapper.findAll("table tbody tr").length).to.equals(2);
expect(wrapper.find(".ui-limitloader").text()).to.contain("only the first 2 entries");
});
});
@@ -0,0 +1,304 @@
<template>
<upload-wrapper ref="wrapper" :top-info="topInfo" :hightlight-box="hightlightBox">
<div class="upload-helper" v-show="showHelper"><i class="fa fa-files-o" />Drop files here</div>
<table class="upload-table ui-table-striped" v-show="!showHelper" ref="uploadTable">
<thead>
<tr>
<th>Name</th>
<th>Size</th>
<th>Status</th>
<th />
</tr>
</thead>
<tbody />
</table>
<template v-slot:footer>
<span class="upload-footer-title">Collection Type:</span>
<select2
container-class="upload-footer-collection-type"
ref="footerCollectionType"
v-model="collectionType"
:enabled="!running"
>
<option value="list">List</option>
<option value="paired">Pair</option>
<option value="list:paired">List of Pairs</option>
</select2>
<span class="upload-footer-title">File Type:</span>
<select2
container-class="upload-footer-extension"
ref="footerExtension"
v-model="extension"
:enabled="!running"
>
<option v-for="(ext, index) in extensions" :key="index" :value="ext.id">{{ ext.text }}</option>
</select2>
<span class="upload-footer-extension-info upload-icon-button fa fa-search" />
<span class="upload-footer-title">Genome (set all):</span>
<select2 container-class="upload-footer-genome" ref="footerGenome" v-model="genome" :enabled="!running">
<option v-for="(listGenome, index) in listGenomes" :key="index" :value="listGenome.id">{{
listGenome.text
}}</option>
</select2>
</template>
<template v-slot:buttons>
<b-button ref="btnClose" class="ui-button-default" id="btn-close" @click="app.hide()">
{{ btnCloseTitle }}
</b-button>
<b-button
ref="btnReset"
class="ui-button-default"
id="btn-reset"
@click="_eventReset"
:disabled="!enableReset"
>
{{ btnResetTitle }}
</b-button>
<b-button
ref="btnStop"
class="ui-button-default"
id="btn-stop"
@click="_eventStop"
:disabled="counterRunning == 0"
>
{{ btnStopTitle }}
</b-button>
<b-button
ref="btnBuild"
class="ui-button-default"
id="btn-build"
@click="_eventBuild"
:disabled="!enableBuild"
:variant="enableBuild ? 'primary' : ''"
>
{{ btnBuildTitle }}
</b-button>
<b-button
ref="btnStart"
class="ui-button-default"
id="btn-start"
@click="_eventStart"
:disabled="!enableStart"
:variant="enableStart ? 'primary' : ''"
>
{{ btnStartTitle }}
</b-button>
<b-button
ref="btnCreate"
class="ui-button-default"
id="btn-new"
@click="_eventCreate"
:disabled="!enableSources"
>
<span class="fa fa-edit"></span>{{ btnCreateTitle }}
</b-button>
<b-button
ref="btnFtp"
class="ui-button-default"
id="btn-ftp"
@click="_eventFtp"
:disabled="!enableSources"
v-if="ftpUploadSite"
>
<span class="fa fa-folder-open-o"></span>{{ btnFtpTitle }}
</b-button>
<b-button
ref="btnLocal"
class="ui-button-default"
id="btn-local"
:title="btnLocalTitle"
@click="uploadSelect"
:disabled="!enableSources"
>
<span class="fa fa-laptop"></span>{{ btnLocalTitle }}
</b-button>
</template>
</upload-wrapper>
</template>
<script>
import _l from "utils/localization";
import _ from "underscore";
import { getGalaxyInstance } from "app";
import UploadRow from "mvc/upload/collection/collection-row";
import UploadBoxMixin from "./UploadBoxMixin";
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
Vue.use(BootstrapVue);
export default {
mixins: [UploadBoxMixin],
data() {
return {
topInfo: "",
showHelper: true,
extension: this.app.defaultExtension,
genome: this.app.defaultGenome,
collectionType: "list",
listExtensions: [],
listGenomes: [],
running: false,
counterAnnounce: 0,
counterSuccess: 0,
counterError: 0,
counterRunning: 0,
uploadSize: 0,
uploadCompleted: 0,
enableReset: false,
enableStart: false,
enableSources: false,
enableBuild: false,
hightlightBox: false,
rowUploadModel: UploadRow,
btnLocalTitle: _l("Choose local files"),
btnCreateTitle: _l("Paste/Fetch data"),
btnFtpTitle: _l("Choose FTP files"),
btnStartTitle: _l("Start"),
btnBuildTitle: _l("Build"),
btnStopTitle: _l("Pause"),
btnResetTitle: _l("Reset"),
btnCloseTitle: _l("Close")
};
},
created() {
this.initCollection();
this.initAppProperties();
},
mounted() {
this.initExtensionInfo();
this.initFtpPopover();
// file upload
this.initUploadbox({
url: this.app.uploadPath,
announce: (index, file) => {
this._eventAnnounce(index, file);
},
initialize: index => {
return this.app.toData([this.collection.get(index)], this.history_id);
},
progress: (index, percentage) => {
this._eventProgress(index, percentage);
},
success: (index, message) => {
this._eventSuccess(index, message);
},
warning: (index, message) => {
this._eventWarning(index, message);
},
error: (index, message) => {
this._eventError(index, message);
},
complete: () => {
this._eventComplete();
},
ondragover: () => {
this.hightlightBox = true;
},
ondragleave: () => {
this.hightlightBox = false;
}
});
this.collection.on("remove", model => {
this._eventRemove(model);
});
this._updateStateForCounters();
},
computed: {
extensions() {
const result = _.filter(this.listExtensions, ext => !ext.composite_files);
return result;
},
appModel() {
return this.app.model;
}
},
watch: {
extension: function(value) {
this.updateExtension(value);
},
genome: function(value) {
this.updateGenome(value);
}
},
methods: {
_newUploadModelProps: function(index, file) {
return {
id: index,
file_name: file.name,
file_size: file.size,
file_mode: file.mode || "local",
file_path: file.path,
file_data: file,
extension: this.extension,
genome: this.genome
};
},
/** Success */
_eventSuccess: function(index, message) {
var hids = _.pluck(message["outputs"], "hid");
var it = this.collection.get(index);
it.set({ percentage: 100, status: "success", hids: hids });
this._updateStateForSuccess(it);
const Galaxy = getGalaxyInstance();
Galaxy.currHistoryPanel.refreshContents();
},
_eventBuild: function() {
const Galaxy = getGalaxyInstance();
var allHids = [];
_.forEach(this.collection.models, upload => {
allHids.push.apply(allHids, upload.get("hids"));
});
var models = _.map(allHids, hid => Galaxy.currHistoryPanel.collection.getByHid(hid));
var selection = new Galaxy.currHistoryPanel.collection.constructor(models);
// I'm building the selection wrong because I need to set this historyId directly.
selection.historyId = Galaxy.currHistoryPanel.collection.historyId;
Galaxy.currHistoryPanel.buildCollection(this.collectionType, selection, true);
this.counterRunning = 0;
this._updateStateForCounters();
this._eventReset();
this.app.hide();
},
/** Start upload process */
_eventStart: function() {
if (this.counterAnnounce == 0 || this.counterRunning > 0) {
return;
}
this.uploadSize = 0;
this.uploadCompleted = 0;
this.collection.each(model => {
if (model.get("status") == "init") {
model.set("status", "queued");
this.uploadSize += model.get("file_size");
}
});
this.appModel.set({ percentage: 0, status: "success" });
this.counterRunning = this.counterAnnounce;
this.history_id = this.app.currentHistory();
// package ftp files separately, and remove them from queue
this._uploadFtp();
this.uploadbox.start();
this._updateStateForCounters();
},
/** Remove all */
_eventReset: function() {
if (this.counterRunning == 0) {
this.collection.reset();
this.counterAnnounce = 0;
this.counterSuccess = 0;
this.counterError = 0;
this.counterRunning = 0;
this.uploadbox.reset();
this.extension = this.app.defaultExtension;
this.genome = this.app.defaultGenome;
this.appModel.set("percentage", 0);
this._updateStateForCounters();
}
}
}
};
</script>
@@ -0,0 +1,16 @@
import Composite from "./Composite.vue";
import { mountWithApp } from "./test_helpers";
describe("Composite.vue", () => {
it("loads with correct initial state", async () => {
const { wrapper } = mountWithApp(Composite);
expect(wrapper.find("#btn-start").classes()).to.contain("disabled");
expect(wrapper.vm.showHelper).to.equals(true);
expect(wrapper.vm.readyStart).to.equals(false);
// filters listExtensions to just the composite definition + _select_ value
const extensions = wrapper.vm.extensions;
expect(extensions.length).to.equals(2);
expect(extensions[0].id).to.equals("_select_");
expect(extensions[1].id).to.equals("affybatch");
});
});
@@ -0,0 +1,209 @@
<template>
<upload-wrapper wrapper-class="upload-view-composite">
<div class="upload-helper" v-show="showHelper">Select Composite Type to show files required to upload</div>
<table class="upload-table ui-table-striped" v-show="!showHelper" ref="uploadTable">
<thead>
<tr>
<th />
<th />
<th>Description</th>
<th>Name</th>
<th>Size</th>
<th>Settings</th>
<th>Status</th>
</tr>
</thead>
<tbody />
</table>
<template v-slot:footer>
<span class="upload-footer-title">Composite Type:</span>
<select2
container-class="upload-footer-extension"
ref="footerExtension"
v-model="extension"
:enabled="!running"
>
<option v-for="(ext, index) in extensions" :key="index" :value="ext.id">{{ ext.text }}</option>
</select2>
<span class="upload-footer-extension-info upload-icon-button fa fa-search" ref="footerExtensionInfo" />
<span class="upload-footer-title">Genome/Build:</span>
<select2 container-class="upload-footer-genome" ref="footerGenome" v-model="genome" :enabled="!running">
<option v-for="(listGenome, index) in listGenomes" :key="index" :value="listGenome.id">{{
listGenome.text
}}</option>
</select2>
</template>
<template v-slot:buttons>
<b-button ref="btnClose" class="ui-button-default" @click="app.hide()">
{{ btnCloseTitle }}
</b-button>
<b-button
ref="btnStart"
class="ui-button-default"
@click="_eventStart"
id="btn-start"
:disabled="!readyStart"
:variant="readyStart ? 'primary' : ''"
>
{{ btnStartTitle }}
</b-button>
<b-button ref="btnReset" class="ui-button-default" id="btn-reset" @click="_eventReset">
{{ btnResetTitle }}
</b-button>
</template>
</upload-wrapper>
</template>
<script>
import _l from "utils/localization";
import _ from "underscore";
import $ from "jquery";
import { getGalaxyInstance } from "app";
import UploadRow from "mvc/upload/composite/composite-row";
import UploadBoxMixin from "./UploadBoxMixin";
export default {
mixins: [UploadBoxMixin],
data() {
return {
extension: "_select_",
genome: this.app.defaultGenome,
listExtensions: [],
listGenomes: [],
running: false,
showHelper: true,
btnResetTitle: _l("Reset"),
btnStartTitle: _l("Start"),
btnCloseTitle: _l("Close"),
readyStart: false
};
},
created() {
this.initCollection();
this.initAppProperties();
},
computed: {
extensions() {
const result = _.filter(this.listExtensions, ext => ext.composite_files);
result.unshift({ id: "_select_", text: "Select" });
return result;
}
},
mounted() {
this.initExtensionInfo();
// listener for collection triggers on change in composite datatype and extension selection
this.collection.on("add", model => {
this._eventAnnounce(model);
});
this.collection.on("change add", () => {
this.renderNonReactiveComponents();
});
this.renderNonReactiveComponents();
},
methods: {
renderNonReactiveComponents: function() {
var model = this.collection.first();
if (model && model.get("status") == "running") {
this.running = true;
} else {
this.running = false;
}
if (
this.collection.where({ status: "ready" }).length + this.collection.where({ optional: true }).length ==
this.collection.length &&
this.collection.length > 0
) {
this.readyStart = true;
} else {
this.readyStart = false;
}
this.showHelper = this.collection.length == 0;
},
//
// upload events / process pipeline
//
/** Builds the basic ui with placeholder rows for each composite data type file */
_eventAnnounce: function(model) {
var upload_row = new UploadRow(this, { model: model });
this.$uploadTable()
.find("tbody:first")
.append(upload_row.$el);
this.showHelper = this.collection.length == 0;
upload_row.render();
},
/** Start upload process */
_eventStart: function() {
this.collection.each(model => {
model.set({
genome: this.genome,
extension: this.extension
});
});
$.uploadpost({
url: this.app.uploadPath,
data: this.app.toData(this.collection.filter()),
success: message => {
this._eventSuccess(message);
},
error: message => {
this._eventError(message);
},
progress: percentage => {
this._eventProgress(percentage);
}
});
},
/** Remove all */
_eventReset: function() {
if (this.collection.where({ status: "running" }).length == 0) {
this.collection.reset();
this.extension = this.app.defaultExtension;
this.genome = this.app.defaultGenome;
this.renderNonReactiveComponents();
}
},
/** Refresh progress state */
_eventProgress: function(percentage) {
this.collection.each(it => {
it.set("percentage", percentage);
});
},
/** Refresh success state */
_eventSuccess: function(message) {
const Galaxy = getGalaxyInstance();
this.collection.each(it => {
it.set("status", "success");
});
Galaxy.currHistoryPanel.refreshContents();
},
/** Refresh error state */
_eventError: function(message) {
this.collection.each(it => {
it.set({ status: "error", info: message });
});
}
},
watch: {
extension: function(value) {
this.collection.reset();
const details = this.extensionDetails(value);
if (details && details.composite_files) {
_.each(details.composite_files, item => {
this.collection.add({
id: this.collection.size(),
file_desc: item.description || item.name,
optional: item.optional
});
});
}
}
}
};
</script>
@@ -0,0 +1,58 @@
import Default from "./Default.vue";
import { mountWithApp } from "./test_helpers";
import { __RewireAPI__ as rewire } from "./Default";
import Backbone from "backbone";
describe("Default.vue", () => {
beforeEach(() => {
// Use of select2 in default-row requires this mock.
rewire.__Rewire__("UploadRow", Backbone.View);
});
it("loads with correct initial state", async () => {
const { wrapper } = mountWithApp(Default);
expect(wrapper.vm.counterAnnounce).to.equals(0);
expect(wrapper.vm.showHelper).to.equals(true);
expect(wrapper.vm.extensions[0].id).to.equals("ab1");
expect(wrapper.find("#btn-reset").classes()).to.contain("disabled");
expect(wrapper.find("#btn-start").classes()).to.contain("disabled");
expect(wrapper.find("#btn-stop").classes()).to.contain("disabled");
});
it("does render FTP is site set", async () => {
const { wrapper, localVue } = mountWithApp(Default);
expect(wrapper.find("#btn-ftp").isVisible()).to.equals(true);
wrapper.find("#btn-ftp").trigger("click");
await localVue.nextTick();
// TODO: test popover appears... not sure best way to do this...
});
it("doesn't render FTP is no site set", async () => {
const { wrapper } = mountWithApp(Default, {
currentFtp: () => {
return null;
}
});
expect(wrapper.findAll("#btn-ftp").length).to.equals(0);
});
it("resets properly", async () => {
const { wrapper, localVue } = mountWithApp(Default);
expect(wrapper.vm.showHelper).to.equals(true);
await localVue.nextTick();
wrapper.find("#btn-new").trigger("click");
await localVue.nextTick();
expect(wrapper.vm.showHelper).to.equals(false);
expect(wrapper.vm.counterAnnounce).to.equals(1);
wrapper.find("#btn-reset").trigger("click");
await localVue.nextTick();
expect(wrapper.vm.showHelper).to.equals(true);
});
it("renders a limitloader element if lazyLoadMax set", async () => {
const { wrapper } = mountWithApp(Default, {}, { lazyLoadMax: 2 });
expect(wrapper.findAll(".ui-limitloader").length).to.equals(1);
// hard to actually test the functionality like in Collection.test.js
// because we're stubbing out all of UploadRow.
});
});
@@ -0,0 +1,267 @@
<template>
<upload-wrapper ref="wrapper" :top-info="topInfo">
<div class="upload-helper" v-show="showHelper"><i class="fa fa-files-o" />Drop files here</div>
<table class="upload-table ui-table-striped" v-show="!showHelper" ref="uploadTable">
<thead>
<tr>
<th>Name</th>
<th>Size</th>
<th>Type</th>
<th>Genome</th>
<th>Settings</th>
<th>Status</th>
<th />
</tr>
</thead>
<tbody />
</table>
<template v-slot:footer>
<span class="upload-footer-title">Type (set all):</span>
<select2
container-class="upload-footer-extension"
ref="footerExtension"
v-model="extension"
:enabled="!running"
>
<option v-for="(ext, index) in extensions" :key="index" :value="ext.id">{{ ext.text }}</option>
</select2>
<span class="upload-footer-extension-info upload-icon-button fa fa-search" />
<span class="upload-footer-title">Genome (set all):</span>
<select2 container-class="upload-footer-genome" ref="footerGenome" v-model="genome" :enabled="!running">
<option v-for="(listGenome, index) in listGenomes" :key="index" :value="listGenome.id">{{
listGenome.text
}}</option>
</select2>
</template>
<template v-slot:buttons>
<b-button ref="btnClose" class="ui-button-default" id="btn-close" @click="app.hide()">
{{ btnCloseTitle }}
</b-button>
<b-button
ref="btnReset"
class="ui-button-default"
id="btn-reset"
@click="_eventReset"
:disabled="!enableReset"
>
{{ btnResetTitle }}
</b-button>
<b-button
ref="btnStop"
class="ui-button-default"
id="btn-stop"
@click="_eventStop"
:disabled="counterRunning == 0"
>
{{ btnStopTitle }}
</b-button>
<b-button
ref="btnStart"
class="ui-button-default"
id="btn-start"
@click="_eventStart"
:disabled="!enableStart"
:variant="enableStart ? 'primary' : ''"
>
{{ btnStartTitle }}
</b-button>
<b-button
ref="btnCreate"
class="ui-button-default"
id="btn-new"
@click="_eventCreate"
:disabled="!enableSources"
>
<span class="fa fa-edit"></span>{{ btnCreateTitle }}
</b-button>
<b-button
ref="btnFtp"
class="ui-button-default"
id="btn-ftp"
@click="_eventFtp"
:disabled="!enableSources"
v-if="ftpUploadSite"
>
<span class="fa fa-folder-open-o"></span>{{ btnFtpTitle }}
</b-button>
<b-button
ref="btnLocal"
class="ui-button-default"
id="btn-local"
:title="btnLocalTitle"
@click="uploadSelect"
:disabled="!enableSources"
>
<span class="fa fa-laptop"></span>{{ btnLocalTitle }}
</b-button>
</template>
</upload-wrapper>
</template>
<script>
import _l from "utils/localization";
import _ from "underscore";
import { getGalaxyInstance } from "app";
import UploadRow from "mvc/upload/default/default-row";
import UploadBoxMixin from "./UploadBoxMixin";
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
Vue.use(BootstrapVue);
export default {
mixins: [UploadBoxMixin],
data() {
return {
topInfo: "",
showHelper: true,
extension: this.app.defaultExtension,
genome: this.app.defaultGenome,
listExtensions: [],
listGenomes: [],
running: false,
rowUploadModel: UploadRow,
counterAnnounce: 0,
counterSuccess: 0,
counterError: 0,
counterRunning: 0,
uploadSize: 0,
uploadCompleted: 0,
enableReset: false,
enableStart: false,
enableSources: false,
btnLocalTitle: _l("Choose local files"),
btnCreateTitle: _l("Paste/Fetch data"),
btnFtpTitle: _l("Choose FTP files"),
btnStartTitle: _l("Start"),
btnStopTitle: _l("Pause"),
btnResetTitle: _l("Reset"),
btnCloseTitle: _l("Close")
};
},
created() {
this.initCollection();
this.initAppProperties();
},
mounted() {
this.initExtensionInfo();
this.initFtpPopover();
// file upload
this.initUploadbox({
url: this.app.uploadPath,
announce: (index, file) => {
this._eventAnnounce(index, file);
},
initialize: index => {
return this.app.toData([this.collection.get(index)], this.history_id);
},
progress: (index, percentage) => {
this._eventProgress(index, percentage);
},
success: (index, message) => {
this._eventSuccess(index, message);
},
error: (index, message) => {
this._eventError(index, message);
},
warning: (index, message) => {
this._eventWarning(index, message);
},
complete: () => {
this._eventComplete();
},
ondragover: () => {
this.hightlightBox = true;
},
ondragleave: () => {
this.hightlightBox = false;
}
});
this.collection.on("remove", model => {
this._eventRemove(model);
});
this._updateStateForCounters();
},
computed: {
extensions() {
const result = _.filter(this.listExtensions, ext => !ext.composite_files);
return result;
},
appModel() {
return this.app.model;
}
},
watch: {
extension: function(value) {
this.updateExtension(value);
},
genome: function(value) {
this.updateGenome(value);
}
},
methods: {
_newUploadModelProps: function(index, file) {
return {
id: index,
file_name: file.name,
file_size: file.size,
file_mode: file.mode || "local",
file_path: file.path,
file_data: file
};
},
/** Success */
_eventSuccess: function(index, message) {
var it = this.collection.get(index);
it.set({ percentage: 100, status: "success" });
this._updateStateForSuccess(it);
},
/** Start upload process */
_eventStart: function() {
if (this.counterAnnounce !== 0 && this.counterRunning === 0) {
// prepare upload process
this.uploadSize = 0;
this.uploadCompleted = 0;
this.collection.each(model => {
if (model.get("status") == "init") {
model.set("status", "queued");
this.uploadSize += model.get("file_size");
}
});
this.appModel.set({ percentage: 0, status: "success" });
this.counterRunning = this.counterAnnounce;
this.history_id = this.app.currentHistory();
// package ftp files separately, and remove them from queue
this._uploadFtp();
// queue remaining files
const Galaxy = getGalaxyInstance();
this.uploadbox.start({
id: Galaxy.user.id,
chunk_upload_size: this.app.chunkUploadSize
});
this._updateStateForCounters();
}
},
/** Remove all */
_eventReset: function() {
if (this.counterRunning === 0) {
this.collection.reset();
this.counterAnnounce = 0;
this.counterSuccess = 0;
this.counterError = 0;
this.counterRunning = 0;
this.uploadbox.reset();
this.extension = this.app.defaultExtension;
this.genome = this.app.defaultGenome;
this.appModel.set("percentage", 0);
this._updateStateForCounters();
}
}
}
};
</script>
@@ -0,0 +1,9 @@
import RulesInput from "./RulesInput.vue";
import { mountWithApp } from "./test_helpers";
describe("RulesInput.vue", () => {
it("loads with correct initial state", async () => {
const { wrapper } = mountWithApp(RulesInput);
expect(wrapper.find("#btn-reset").classes()).to.contain("disabled");
});
});
@@ -0,0 +1,177 @@
<template>
<upload-wrapper ref="wrapper" :top-info="topInfo">
<span style="width: 25%; display: inline; height: 100%" class="float-left">
<div class="upload-rule-option">
<div class="upload-rule-option-title">{{ l("Upload data as") }}</div>
<div class="rule-data-type">
<select2 v-model="dataType" container-class="upload-footer-selection">
<option value="datasets">Datasets</option>
<option value="collections">Collection(s)</option>
</select2>
</div>
</div>
<div class="upload-rule-option">
<div class="upload-rule-option-title">{{ l("Load tabular data from") }}</div>
<div class="rule-select-type">
<select2 container-class="upload-footer-selection" v-model="selectionType">
<option value="paste">{{ l("Pasted Table") }}</option>
<option value="dataset">{{ l("History Dataset") }}</option>
<option v-if="ftpUploadSite" value="ftp">{{ l("FTP Directory") }} </option></select2
>
</div>
</div>
<div id="upload-rule-dataset-option" class="upload-rule-option" v-if="selectionType == 'dataset'">
<div class="upload-rule-option-title">{{ l("Select dataset to load") }}</div>
<div class="dataset-selector">
<select2
v-model="selectedDatasetId"
container-class="upload-footer-selection"
placeholder="Select Dataset"
>
<option></option>
<option v-for="dataset of datasets" :key="dataset.id" :value="dataset.id">
{{ dataset.hid }}: {{ dataset.name }}</option
>
</select2>
</div>
</div>
</span>
<span style="display: inline; float: right; width: 75%; height: 300px">
<textarea
class="upload-rule-source-content form-control"
style="height: 100%"
v-model="sourceContent"
:disabled="selectionType != 'paste'"
></textarea>
</span>
<template v-slot:buttons>
<b-button ref="btnClose" class="ui-button-default" id="btn-close" @click="app.hide()">
{{ btnCloseTitle }}
</b-button>
<b-button
ref="btnBuild"
class="ui-button-default"
id="btn-build"
@click="_eventBuild"
:disabled="!sourceContent"
:variant="sourceContent ? 'primary' : ''"
>
{{ btnBuildTitle }}
</b-button>
<b-button
ref="btnReset"
class="ui-button-default"
id="btn-reset"
@click="_eventReset"
:disabled="!enableReset"
>
{{ btnResetTitle }}
</b-button>
</template>
</upload-wrapper>
</template>
<script>
import _l from "utils/localization";
import { getGalaxyInstance } from "app";
import UploadBoxMixin from "./UploadBoxMixin";
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import UploadUtils from "mvc/upload/upload-utils";
import axios from "axios";
import { getAppRoot } from "onload/loadConfig";
Vue.use(BootstrapVue);
export default {
mixins: [UploadBoxMixin],
data() {
return {
l: _l,
datasets: [],
ftpFiles: [],
datasetsSet: false,
topInfo: _l("Tabular source data to extract collection files and metadata from"),
enableReset: false,
enableBuild: false,
dataType: "datasets",
selectedDatasetId: null,
sourceContent: "",
selectionType: "paste",
btnBuildTitle: _l("Build"),
btnResetTitle: _l("Reset"),
btnCloseTitle: _l("Close")
};
},
created() {
this.initCollection();
this.initAppProperties();
},
watch: {
selectionType: function(selectionType) {
if (selectionType == "dataset" && !this.datasetsSet) {
const Galaxy = getGalaxyInstance();
const history = Galaxy && Galaxy.currHistoryPanel && Galaxy.currHistoryPanel.model;
const historyContentModels = history.contents.models;
for (const historyContentModel of historyContentModels) {
const attr = historyContentModel.attributes;
if (attr.history_content_type !== "dataset") {
continue;
}
this.datasets.push(attr);
}
this.datasetsSet = true;
} else if (selectionType == "ftp") {
UploadUtils.getRemoteFiles(ftp_files => {
this.sourceContent = ftp_files.map(file => file["path"]).join("\n");
this.ftpFiles = ftp_files;
});
}
},
selectedDatasetId: function(selectedDatasetId) {
if (!selectedDatasetId) {
this.sourceContent = "";
return;
}
const Galaxy = getGalaxyInstance();
axios
.get(
`${getAppRoot()}api/histories/${
Galaxy.currHistoryPanel.model.id
}/contents/${selectedDatasetId}/display`
)
.then(response => {
this.sourceContent = response.data;
})
.catch(error => console.log(error));
}
},
methods: {
_eventReset: function() {
this.selectedDatasetId = null;
this.sourceContent = "";
},
_eventBuild: function() {
this._buildSelection(this.sourceContent);
},
_buildSelection: function(content) {
const selectionType = this.selectionType;
const selection = {};
const Galaxy = getGalaxyInstance();
if (selectionType == "dataset" || selectionType == "paste") {
selection.selectionType = "raw";
selection.content = content;
} else if (selectionType == "ftp") {
selection.selectionType = "ftp";
selection.elements = this.ftpFiles;
selection.ftpUploadSite = this.ftpUploadSite;
}
selection.dataType = this.dataType;
Galaxy.currHistoryPanel.buildCollection("rules", selection, true);
this.app.hide();
}
}
};
</script>
@@ -0,0 +1,291 @@
import _l from "utils/localization";
import _ from "underscore";
import $ from "jquery";
import Select2 from "components/Select2";
import Popover from "mvc/ui/ui-popover";
import UploadExtension from "mvc/upload/upload-extension";
import UploadModel from "mvc/upload/upload-model";
import UploadWrapper from "./UploadWrapper";
import { getGalaxyInstance } from "app";
import UploadFtp from "mvc/upload/upload-ftp";
import LazyLimited from "mvc/lazy/lazy-limited";
export default {
components: {
UploadWrapper,
Select2
},
props: {
app: {
type: Object,
required: true
},
lazyLoadMax: {
type: Number,
default: null
}
},
methods: {
$uploadBox() {
return $(this.$refs.wrapper.$refs.uploadBox);
},
initUploadbox(options) {
const $uploadBox = this.$uploadBox();
this.uploadbox = $uploadBox.uploadbox(options);
if (this.lazyLoadMax !== null) {
const $uploadBox = this.$uploadBox();
this.loader = new LazyLimited({
$container: $uploadBox,
collection: this.collection,
max: this.lazyLoadMax,
new_content: model => {
return this.renderNewModel(model);
}
});
}
},
uploadSelect: function() {
this.uploadbox.select();
},
/** Package and upload ftp files in a single request */
_uploadFtp: function() {
const list = [];
this.collection.each(model => {
if (model.get("status") == "queued" && model.get("file_mode") == "ftp") {
this.uploadbox.remove(model.id);
list.push(model);
}
});
if (list.length > 0) {
$.uploadpost({
data: this.app.toData(list),
url: this.app.uploadPath,
success: message => {
_.each(list, model => {
this._eventSuccess(model.id, message);
});
},
error: message => {
_.each(list, model => {
self._eventError(model.id, message);
});
}
});
}
},
renderNewModel: function(model) {
// Turn backbone upload model into row model and place in table,
// render in next tick so table can respond first and dynamic
// sizing works.
var uploadRow = new this.rowUploadModel(this, { model: model });
this.$uploadTable()
.find("tbody:first")
.append(uploadRow.$el);
this._updateStateForCounters();
this.$nextTick(() => {
uploadRow.render();
});
return uploadRow;
},
_updateStateForCounters: function() {
this.setTopInfoBasedOnCounters();
this.enableReset =
this.counterRunning == 0 && this.counterAnnounce + this.counterSuccess + this.counterError > 0;
this.enableStart = this.counterRunning == 0 && this.counterAnnounce > 0;
this.enableBuild =
this.counterRunning == 0 &&
this.counterAnnounce == 0 &&
this.counterSuccess > 0 &&
this.counterError == 0;
this.enableSources = this.counterRunning == 0;
var show_table = this.counterAnnounce + this.counterSuccess + this.counterError > 0;
this.showHelper = !show_table;
},
setTopInfoBasedOnCounters: function() {
var message = "";
if (this.counterAnnounce == 0) {
if (this.uploadbox.compatible()) {
message = "&nbsp;";
} else {
message =
"Browser does not support Drag & Drop. Try Firefox 4+, Chrome 7+, IE 10+, Opera 12+ or Safari 6+.";
}
} else {
if (this.counterRunning == 0) {
message = `You added ${this.counterAnnounce} file(s) to the queue. Add more files or click 'Start' to proceed.`;
} else {
message = `Please wait...${this.counterAnnounce} out of ${this.counterRunning} remaining.`;
}
}
this.topInfo = message;
},
/** Progress */
_eventProgress: function(index, percentage) {
var it = this.collection.get(index);
it.set("percentage", percentage);
this.appModel.set("percentage", this._uploadPercentage(percentage, it.get("file_size")));
},
/** Calculate percentage of all queued uploads */
_uploadPercentage: function(percentage, size) {
return (this.uploadCompleted + percentage * size) / this.uploadSize;
},
_updateStateForSuccess: function(model) {
this.appModel.set("percentage", this._uploadPercentage(100, model.get("file_size")));
this.uploadCompleted += model.get("file_size") * 100;
this.counterAnnounce--;
this.counterSuccess++;
this._updateStateForCounters();
const Galaxy = getGalaxyInstance();
Galaxy.currHistoryPanel.refreshContents();
},
/** A new file has been dropped/selected through the uploadbox plugin */
_eventAnnounce: function(index, file) {
this.counterAnnounce++;
const modelProps = this._newUploadModelProps(index, file);
var newModel = new UploadModel.Model(modelProps);
this.collection.add(newModel);
// if using lazyLoader, let it handle it - else render directly
if (this.loader) {
this._updateStateForCounters();
} else {
this.renderNewModel(newModel);
}
},
/** Error */
_eventError: function(index, message) {
var it = this.collection.get(index);
it.set({ percentage: 100, status: "error", info: message });
this.appModel.set({
percentage: this._uploadPercentage(100, it.get("file_size")),
status: "danger"
});
this.uploadCompleted += it.get("file_size") * 100;
this.counterAnnounce--;
this.counterError++;
this._updateStateForCounters();
},
/** Queue is done */
_eventComplete: function() {
this.collection.each(model => {
model.get("status") == "queued" && model.set("status", "init");
});
this.counterRunning = 0;
this._updateStateForCounters();
},
/** Remove model from upload list */
_eventRemove: function(model) {
var status = model.get("status");
if (status == "success") {
this.counterSuccess--;
} else if (status == "error") {
this.counterError--;
} else {
this.counterAnnounce--;
}
this.uploadbox.remove(model.id);
this._updateStateForCounters();
},
/** Show/hide ftp popup */
_eventFtp: function() {
this.ftp.show(
new UploadFtp({
collection: this.collection,
ftp_upload_site: this.ftpUploadSite,
onadd: ftp_file => {
return this.uploadbox.add([
{
mode: "ftp",
name: ftp_file.path,
size: ftp_file.size,
path: ftp_file.path
}
]);
},
onremove: function(model_index) {
this.collection.remove(model_index);
}
}).$el
);
},
/** Create a new file */
_eventCreate: function() {
this.uploadbox.add([{ name: "New File", size: 0, mode: "new" }]);
},
/** Pause upload process */
_eventStop: function() {
if (this.counterRunning > 0) {
this.appModel.set("status", "info");
this.topInfo = "Queue will pause after completing the current file...";
this.uploadbox.stop();
}
},
_eventWarning: function(index, message) {
var it = this.collection.get(index);
it.set({ status: "warning", info: message });
},
$uploadTable() {
return $(this.$refs.uploadTable);
},
extensionDetails(extension) {
var details = _.findWhere(this.listExtensions, {
id: extension
});
return details;
},
initExtensionInfo() {
$(this.$refs.footerExtensionInfo)
.on("click", e => {
const details = this.extensionDetails(this.extension);
if (details) {
new UploadExtension({
$el: $(e.target),
title: details && details.text,
extension: details && details.id,
list: this.listExtensions,
placement: "top"
});
}
})
.on("mousedown", e => {
e.preventDefault();
});
},
initCollection() {
this.collection = new UploadModel.Collection();
},
initAppProperties() {
this.listExtensions = this.app.listExtensions;
this.listGenomes = this.app.listGenomes;
this.ftpUploadSite = this.app.currentFtp();
},
initFtpPopover() {
// add ftp file viewer
this.ftp = new Popover({
title: _l("FTP files"),
container: $(this.$refs.btnFtp)
});
},
/* walk collection and update un-modified default values when globals
change */
updateExtension(extension, defaults_only) {
this.collection.each(model => {
if (
model.get("status") == "init" &&
(model.get("extension") == this.app.defaultExtension || !defaults_only)
) {
model.set("extension", extension);
}
});
},
updateGenome: function(genome, defaults_only) {
this.collection.each(model => {
if (
model.get("status") == "init" &&
(model.get("genome") == this.app.defaultGenome || !defaults_only)
) {
model.set("genome", genome);
}
});
}
}
};
@@ -0,0 +1,66 @@
import MockAdapter from "axios-mock-adapter";
import axios from "axios";
import UploadModal from "./UploadModal.vue";
import { setupTestGalaxy } from "qunit/test-app";
import { shallowMount, createLocalVue } from "@vue/test-utils";
const propsData = {
chunkUploadSize: 1024,
uploadPath: "/api/tools"
};
describe("UploadModal.vue", () => {
let wrapper;
let localVue;
let axiosMock;
beforeEach(() => {
axiosMock = new MockAdapter(axios);
const fastaResponse = {
description_url: "https://wiki.galaxyproject.org/Learn/Datatypes#Fasta",
display_in_upload: true,
extension: "fasta",
description:
"A sequence in FASTA format consists of a single-line description, followed by lines of sequence data. The first character of the description line is a greater-than ('>') symbol in the first column. All lines should be shorter than 80 characters."
};
const datatypesResponse = [fastaResponse];
axiosMock.onGet(`/api/datatypes?extension_only=False`).reply(200, datatypesResponse);
const genomesResponse = [
["Scarlet macaw Jun 2013 (SMACv1.1/araMac1) (araMac1)", "araMac1"],
["Cat Dec. 2008 (NHGRI/GTB V17e/felCat4) (felCat4)", "felCat4"],
["Cat Sep. 2011 (ICGSC Felis_catus 6.2/felCat5) (felCat5)", "felCat5"]
];
axiosMock.onGet(`/api/genomes`).reply(200, genomesResponse);
localVue = createLocalVue();
setupTestGalaxy();
wrapper = shallowMount(UploadModal, {
propsData: propsData,
localVue: localVue
});
});
afterEach(() => {
axiosMock.restore();
});
it("should load with correct defaults", async () => {
expect(wrapper.vm.auto.id).to.equals("auto");
expect(wrapper.vm.datatypesDisableAuto).to.equals(false);
});
it("should fetch datatypes and parse them", async () => {
await localVue.nextTick();
await localVue.nextTick();
expect(wrapper.vm.listExtensions.length).to.equals(2);
expect(wrapper.vm.listExtensions[0].id).to.equals("auto");
expect(wrapper.vm.listExtensions[1].id).to.equals("fasta");
});
it("should fetch genomes and parse them", async () => {
await localVue.nextTick();
await localVue.nextTick();
expect(wrapper.vm.listGenomes.length).to.equals(3);
});
});
@@ -0,0 +1,234 @@
<template>
<b-modal
v-model="modalShow"
:static="modalStatic"
header-class="no-separator"
modal-class="ui-modal"
dialog-class="upload-dialog"
body-class="upload-dialog-body"
no-enforce-focus
hide-footer
>
<template v-slot:modal-header>
<h4 class="title" tabindex="0">{{ title }}</h4>
</template>
<b-tabs v-if="historyAvailable">
<b-tab title="Regular" id="regular" button-id="tab-title-link-regular">
<default :app="this" :lazy-load-max="50" />
</b-tab>
<b-tab title="Composite" id="composite" button-id="tab-title-link-composite">
<composite :app="this" />
</b-tab>
<b-tab title="Collection" id="collection" button-id="tab-title-link-collection">
<collection :app="this" />
</b-tab>
<b-tab title="Rule-based" id="rule-based" button-id="tab-title-link-rule-based">
<rules-input :app="this" />
</b-tab>
</b-tabs>
<div v-else>
Loading required information from Galaxy server.
</div>
</b-modal>
</template>
<script>
import Backbone from "backbone";
import $ from "jquery";
import _l from "utils/localization";
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import { getGalaxyInstance } from "app";
import UploadUtils from "mvc/upload/upload-utils";
import Composite from "./Composite";
import Collection from "./Collection";
import Default from "./Default";
import RulesInput from "./RulesInput";
Vue.use(BootstrapVue);
export default {
components: {
Collection,
Composite,
Default,
RulesInput
},
props: {
modalStatic: {
type: Boolean,
default: true
},
chunkUploadSize: {
type: Number,
required: true
},
uploadPath: {
type: String,
required: true
},
modalShow: {
type: Boolean,
default: false
},
ftpUploadSite: {
type: String,
default: "n/a"
},
defaultGenome: {
type: String,
default: UploadUtils.DEFAULT_GENOME
},
defaultExtension: {
type: String,
default: UploadUtils.DEFAULT_EXTENSION
},
datatypesDisableAuto: {
type: Boolean,
default: false
},
auto: {
type: Object,
default: function() {
return UploadUtils.AUTO_EXTENSION;
}
}
},
data: function() {
return {
title: _l("Download from web or upload from disk"),
historyAvailable: false,
currentUser: null,
listGenomes: [],
listExtensions: []
};
},
created: function() {
this.model = new Backbone.Model({
label: "Load Data",
percentage: 0,
status: "",
onunload: function() {},
onclick: function() {}
});
$(window).on("beforeunload", () => this.model.get("onunload")());
// load extensions
UploadUtils.getUploadDatatypes(
listExtensions => {
this.listExtensions = listExtensions;
},
this.datatypesDisableSuto,
this.auto
);
// load genomes
UploadUtils.getUploadGenomes(listGenomes => {
this.listGenomes = listGenomes;
}, this.defaultGenome);
this.initStateWhenHistoryReady();
},
methods: {
show() {
this.modalShow = true;
this.$nextTick(this.tryMountingTabs);
},
hide() {
this.modalShow = false;
},
initStateWhenHistoryReady() {
const Galaxy = getGalaxyInstance();
if (!Galaxy.currHistoryPanel || !Galaxy.currHistoryPanel.model) {
window.setTimeout(() => {
this.initStateWhenHistoryReady();
}, 500);
return;
}
this.historyAvailable = true;
this.currentUser = Galaxy.user.id;
},
currentFtp: function() {
return this.currentUser && this.ftpUploadSite;
},
/** Refresh user and current history */
currentHistory: function() {
const Galaxy = getGalaxyInstance();
return this.currentUser && Galaxy.currHistoryPanel.model.get("id");
},
/**
* Package API data from array of models
* @param{Array} items - Upload items/rows filtered from a collection
*/
toData: function(items, history_id) {
// create dictionary for data submission
var data = {
payload: {
tool_id: "upload1",
history_id: history_id || this.currentHistory(),
inputs: {}
},
files: [],
error_message: null
};
// add upload tools input data
if (items && items.length > 0) {
var inputs = {
file_count: items.length,
dbkey: items[0].get("genome", "?"),
// sometimes extension set to "" in automated testing after first upload of
// a session. https://github.com/galaxyproject/galaxy/issues/5169
file_type: items[0].get("extension") || "auto"
};
for (var index in items) {
var it = items[index];
it.set("status", "running");
if (it.get("file_size") > 0) {
var prefix = `files_${index}|`;
inputs[`${prefix}type`] = "upload_dataset";
if (it.get("file_name") != "New File") {
inputs[`${prefix}NAME`] = it.get("file_name");
}
inputs[`${prefix}space_to_tab`] = (it.get("space_to_tab") && "Yes") || null;
inputs[`${prefix}to_posix_lines`] = (it.get("to_posix_lines") && "Yes") || null;
inputs[`${prefix}dbkey`] = it.get("genome", null);
inputs[`${prefix}file_type`] = it.get("extension", null);
switch (it.get("file_mode")) {
case "new":
inputs[`${prefix}url_paste`] = it.get("url_paste");
break;
case "ftp":
inputs[`${prefix}ftp_files`] = it.get("file_path");
break;
case "local":
data.files.push({
name: `${prefix}file_data`,
file: it.get("file_data")
});
}
} else if (it.get("optional")) {
continue;
} else {
data.error_message = "Upload content incomplete.";
it.set("status", "error");
it.set("info", data.error_message);
break;
}
}
data.payload.inputs = JSON.stringify(inputs);
}
return data;
}
}
};
</script>
<style>
.upload-dialog {
width: 900px;
}
.upload-dialog-body {
height: 500px;
overflow: hidden;
}
</style>
@@ -0,0 +1,49 @@
<!-- container for an uploadBox -->
<template>
<div :class="wrapperClass">
<div class="upload-top">
<div class="upload-top-info" v-html="topInfo"></div>
</div>
<div class="upload-box" ref="uploadBox" :style="boxStyle" :class="{ highlight: hightlightBox }">
<slot />
</div>
<div class="upload-footer" v-if="hasFooter">
<slot name="footer" />
</div>
<div class="clear" v-else />
<div class="upload-buttons">
<slot name="buttons" />
</div>
</div>
</template>
<script>
export default {
props: {
topInfo: {
type: String,
default: "&nbsp;"
},
wrapperClass: {
type: String,
default: "upload-view-default"
},
hightlightBox: {
type: Boolean,
default: false
}
},
computed: {
hasFooter() {
// https://stackoverflow.com/questions/44077277/only-show-slot-if-it-has-content/50096300#50096300
const name = "footer";
return !!this.$slots[name] || !!this.$scopedSlots[name];
},
boxStyle: function() {
return {
height: this.hasFooter ? "300px" : "335px"
};
}
}
};
</script>
@@ -0,0 +1,45 @@
import _ from "underscore";
import Backbone from "backbone";
import "utils/uploadbox";
import { mount, createLocalVue } from "@vue/test-utils";
export function mountWithApp(component, options = {}, propsData_ = {}) {
const app = _.defaults(options, {
defaultExtension: "auto",
currentFtp: () => {
return "ftp://localhost";
},
model: new Backbone.Model(),
listExtensions: [
{ id: "ab1", text: "ab1", description: "A binary sequence file in 'ab1' format with a '.ab1'" },
{
id: "affybatch",
text: "affybatch",
description: null,
composite_files: [
{
name: "%s.pheno",
optional: false,
description: "Phenodata tab text file"
},
{
name: "%s.affybatch",
optional: false,
description: "AffyBatch R object saved to file"
}
]
}
]
});
const propsData = _.defaults(propsData_, { app });
const localVue = createLocalVue();
const wrapper = mount(component, {
propsData,
localVue,
attachToDocument: true,
stubs: {
select2: true
}
});
return { wrapper, localVue };
}
@@ -1,9 +1,9 @@
<template>
<invocations
:invocationItems="invocationItems"
:invocation-items="invocationItems"
:loading="loading"
headerMessage="Your most recent workflow invocations are displayed on this page."
noInvocationsMessage="There are no workflow invocations to show."
header-message="Your most recent workflow invocations are displayed on this page."
no-invocations-message="There are no workflow invocations to show."
>
</invocations>
</template>
@@ -0,0 +1,177 @@
<template>
<div id="center" class="inbound">
<div class="unified-panel-header" unselectable="on">
<div class="unified-panel-header-inner">
<span class="sr-only">Workflow Editor</span>
{{ editorConfig.name }}
<WorkflowOptions
:canvas="isCanvas"
@onSave="onSave"
@onSaveAs="onSaveAs"
@onRun="onRun"
@onDownload="onDownload"
@onReport="onReport"
@onReportHelp="onReportHelp"
@onLayout="onLayout"
@onEdit="onEdit"
@onAttributes="onAttributes"
/>
</div>
</div>
<div class="unified-panel-body" id="workflow-canvas-body" v-show="isCanvas">
<div id="canvas-viewport" class="workflow-canvas-content">
<div id="canvas-container" />
</div>
<div id="workflow-parameters-box">
<span class="workflow-parameters-box-title">
Workflow Parameters
</span>
<div id="workflow-parameters-container" />
</div>
<div class="workflow-overview" aria-hidden="true">
<div class="workflow-overview-body">
<div id="overview">
<canvas width="0" height="0" id="overview-canvas" />
<div id="overview-viewport" />
</div>
</div>
</div>
</div>
<div class="unified-panel-body workflow-report-body" v-show="!isCanvas">
<markdown-editor ref="report-editor" initial-markdown="" :onupdate="onReportUpdate" :toolbar="false" />
</div>
</div>
</template>
<script>
import WorkflowView from "mvc/workflow/workflow-view";
import WorkflowOptions from "./Options";
import MarkdownEditor from "components/Markdown/MarkdownEditor";
import { getAppRoot } from "onload/loadConfig";
import { showReportHelp } from "./reportHelp";
export default {
components: { MarkdownEditor, WorkflowOptions },
props: {
editorConfig: {
type: Object
}
},
data() {
return {
isCanvas: true
};
},
mounted() {
this.workflowView = new WorkflowView(this.editorConfig, this.$refs["report-editor"]);
},
methods: {
onDownload() {
window.location = `${getAppRoot()}api/workflows/${this.editorConfig.id}/download?format=json-download`;
},
onSaveAs() {
this.workflowView.workflow_save_as();
},
onLayout() {
this.workflowView.layout_editor();
},
onAttributes() {
this.workflowView.workflow.clear_active_node();
},
onEdit() {
this.isCanvas = true;
},
onReport() {
this.isCanvas = false;
},
onReportUpdate(markdown) {
this.workflowView.report_changed(markdown);
},
onRun() {
window.location = `${getAppRoot()}workflows/run?id=${this.editorConfig.id}`;
},
onSave() {
this.workflowView.save_current_workflow();
},
onReportHelp() {
showReportHelp();
}
}
};
</script>
<style scoped>
#center {
z-index: 0;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.workflow-report-body {
display: flex;
}
.workflow-overview-body {
position: relative;
overflow: hidden;
width: 100%;
height: 100%;
border-top: solid gray 1px;
border-left: solid grey 1px;
}
#canvas-container {
position: absolute;
width: 100%;
height: 100%;
}
#workflow-parameters-box {
display: none;
position: absolute;
right: 0px;
border: solid grey 1px;
padding: 5px;
background: #eeeeee;
z-index: 20000;
overflow: auto;
max-width: 300px;
max-height: 300px;
}
.workflow-parameters-box-title {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
#overview {
position: absolute;
}
#overview-canvas {
background: white;
width: 100%;
height: 100%;
}
#overview-viewport {
position: absolute;
width: 0px;
height: 0px;
border: solid blue 1px;
z-index: 10;
}
</style>
<style>
canvas {
position: absolute;
z-index: 10;
}
canvas.dragging {
position: absolute;
z-index: 1000;
}
</style>
@@ -0,0 +1,104 @@
<template>
<div class="panel-header-buttons">
<span v-if="canvas">
<b-button
id="workflow-save-button"
role="button"
title="Save Workflow"
variant="link"
aria-label="Save Workflow"
v-b-tooltip.hover
v-b-tooltip.bottom
@click="$emit('onSave')"
>
<span class="fa fa-floppy-o" />
</b-button>
<b-button
id="workflow-report-button"
role="button"
title="Edit Report"
variant="link"
aria-label="Edit Report"
v-b-tooltip.hover
v-b-tooltip.bottom
@click="$emit('onReport')"
>
<span class="fa fa-edit" />
</b-button>
<b-dropdown
no-caret
right
id="workflow-options-button"
role="button"
title="Workflow Options"
variant="link"
aria-label="Workflow Options"
v-b-tooltip.hover
v-b-tooltip.bottom
>
<template v-slot:button-content>
<span class="fa fa-cog" />
</template>
<b-dropdown-item href="#" @click="$emit('onSaveAs')">Save As</b-dropdown-item>
<b-dropdown-item href="#" @click="$emit('onAttributes')">Edit Attributes</b-dropdown-item>
<b-dropdown-item href="#" @click="$emit('onLayout')">Auto Layout</b-dropdown-item>
<b-dropdown-item href="#" @click="$emit('onDownload')">Download</b-dropdown-item>
</b-dropdown>
<b-button
id="workflow-run-button"
role="button"
title="Run Workflow"
variant="link"
aria-label="Run Workflow"
v-b-tooltip.hover
v-b-tooltip.bottom
@click="$emit('onRun')"
>
<span class="fa fa-play" />
</b-button>
</span>
<span v-else>
<b-button
id="workflow-report-help-button"
role="button"
title="Report Syntax Help"
sm="size"
variant="link"
aria-label="Report Syntax Help"
v-b-tooltip.hover
v-b-tooltip.bottom
@click="$emit('onReportHelp')"
>
<span class="fa fa-question" />
</b-button>
<b-button
id="workflow-canvas-button"
role="button"
title="Return to Workflow"
sm="size"
variant="link"
aria-label="Return to Workflow"
@click="$emit('onEdit')"
>
<span class="fa fa-times" />
</b-button>
</span>
</div>
</template>
<script>
export default {
props: {
canvas: {
type: Boolean,
required: true
}
}
};
</script>
<style>
.dropdown-menu {
z-index: 10000 !important;
}
</style>
@@ -1,236 +0,0 @@
<template>
<div id="center" class="inbound">
<div class="unified-panel-header" unselectable="on">
<div class="unified-panel-header-inner">
<span class="sr-only">Workflow Editor&nbsp;</span>
{{ editorConfig["name"] }}
<div class="panel-header-buttons">
<a
id="workflow-run-button"
class="panel-header-button"
href="javascript:void(0)"
role="button"
title="Run"
aria-label="Run"
@click="navigateToRun"
v-show="mode == 'canvas'"
>
<span class="fa fa-play"></span>
</a>
<a
id="workflow-save-button"
class="panel-header-button"
href="javascript:void(0)"
role="button"
title="Save"
aria-label="Save"
@click="save"
v-show="mode == 'canvas'"
>
<span class="fa fa-floppy-o"></span>
</a>
<a
id="workflow-report-button"
class="panel-header-button"
href="javascript:void(0)"
role="button"
title="Edit Report"
aria-label="Edit Report"
v-show="mode == 'canvas'"
@click="setMode('report')"
>
<span class="fa fa-edit"></span>
</a>
<a
id="workflow-report-help-button"
class="panel-header-button"
href="javascript:void(0)"
role="button"
title="Report Syntax Help"
aria-label="Report Syntax Help"
@click="showReportHelp"
v-show="mode == 'report'"
>
<span class="fa fa-question"></span>
</a>
<a
id="workflow-canvas-button"
class="panel-header-button"
href="javascript:void(0)"
role="button"
title="Edit Workflow"
aria-label="Edit Workflow"
@click="setMode('canvas')"
v-show="mode == 'report'"
>
<span class="fa fa-sitemap fa-rotate-270"></span>
</a>
<a
id="workflow-options-button"
class="panel-header-button"
href="javascript:void(0)"
role="button"
title="Workflow options"
aria-label="Workflow options"
ref="save-button"
v-show="mode == 'canvas'"
>
<span class="fa fa-cog"></span>
</a>
</div>
</div>
</div>
<div class="unified-panel-body" id="workflow-canvas-body" v-show="mode == 'canvas'">
<div id="canvas-viewport" class="workflow-canvas-content">
<div id="canvas-container"></div>
</div>
<div id="workflow-parameters-box">
<span class="workflow-parameters-box-title">
Workflow Parameters
</span>
<div id="workflow-parameters-container"></div>
</div>
<div class="workflow-overview" v-show="mode == 'canvas'" aria-hidden="true">
<div class="workflow-overview-body">
<div id="overview">
<canvas width="0" height="0" id="overview-canvas"></canvas>
<div id="overview-viewport"></div>
</div>
</div>
</div>
</div>
<div class="unified-panel-body workflow-report-body" v-show="mode == 'report'">
<markdown-editor ref="report-editor" initial-markdown="" :onupdate="onReportUpdate" :toolbar="false" />
</div>
</div>
</template>
<script>
import $ from "jquery";
import WorkflowView from "mvc/workflow/workflow-view";
import MarkdownEditor from "components/Markdown/MarkdownEditor";
import { getAppRoot } from "onload/loadConfig";
import { showReportHelp } from "./reportHelp";
import { make_popupmenu } from "ui/popupmenu";
export default {
components: { MarkdownEditor },
props: {
editorConfig: {
type: Object
}
},
data() {
return {
mode: "canvas"
};
},
mounted() {
this.workflowView = new WorkflowView(this.editorConfig, this.$refs["report-editor"]);
make_popupmenu($(this.$refs["save-button"]), {
"Save As": () => this.workflowView.workflow_save_as(),
"Edit Attributes": () => {
this.workflowView.workflow.clear_active_node();
},
"Auto Re-layout": () => this.workflowView.layout_editor(),
Download: {
url: `${getAppRoot()}api/workflows/${this.editorConfig.id}/download?format=json-download`,
action: function() {}
}
});
},
methods: {
setMode(mode) {
this.mode = mode;
},
onReportUpdate(markdown) {
this.workflowView.report_changed(markdown);
},
navigateToRun() {
window.location = `${getAppRoot()}workflows/run?id=${this.editorConfig.id}`;
},
save() {
this.workflowView.save_current_workflow();
},
showReportHelp() {
showReportHelp();
}
}
};
</script>
<style scoped>
#center {
z-index: 0;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.workflow-report-body {
display: flex;
}
.workflow-overview-body {
position: relative;
overflow: hidden;
width: 100%;
height: 100%;
border-top: solid gray 1px;
border-left: solid grey 1px;
}
#canvas-container {
position: absolute;
width: 100%;
height: 100%;
}
#workflow-parameters-box {
display: none;
position: absolute;
right: 0px;
border: solid grey 1px;
padding: 5px;
background: #eeeeee;
z-index: 20000;
overflow: auto;
max-width: 300px;
max-height: 300px;
}
.workflow-parameters-box-title {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
#overview {
position: absolute;
}
#overview-canvas {
background: white;
width: 100%;
height: 100%;
}
#overview-viewport {
position: absolute;
width: 0px;
height: 0px;
border: solid blue 1px;
z-index: 10;
}
</style>
<style>
canvas {
position: absolute;
z-index: 10;
}
canvas.dragging {
position: absolute;
z-index: 1000;
}
</style>
@@ -3,12 +3,12 @@
*/
import Vue from "vue";
import SidePanel from "components/Panels/SidePanel";
import WorkflowEditor from "./WorkflowEditor";
import Index from "./Index";
import WorkflowPanel from "./WorkflowPanel";
export const mountWorkflowEditor = editorConfig => {
const propsData = { editorConfig };
const component = Vue.extend(WorkflowEditor);
const component = Vue.extend(Index);
return new component({ propsData: propsData, el: "#center" });
};
@@ -47,7 +47,7 @@ workflow_display()
<pre>
\`\`\`galaxy
job_parameters(step=qc)
job_parameters(step="qc")
\`\`\`
</pre>
@@ -30,7 +30,7 @@
<template v-slot:row-details="row">
<b-card>
<!-- set provideContext to false, since the table itself provides this information -->
<workflow-invocation-state :invocationId="row.item.id" :provideContext="false" />
<workflow-invocation-state :invocation-id="row.item.id" :provide-context="false" />
</b-card>
</template>
<template v-slot:cell(details)="data">
@@ -55,7 +55,7 @@
</template>
<script>
import { getAppRoot } from "onload/loadConfig";
import { Services } from "./services.js";
import { Services } from "./services";
export default {
props: ["workflow"],
data() {
@@ -88,7 +88,7 @@ import { faSpinner } from "@fortawesome/free-solid-svg-icons";
import { faUpload } from "@fortawesome/free-solid-svg-icons";
import { getAppRoot } from "onload/loadConfig";
import { Services } from "./services.js";
import { Services } from "./services";
import Tags from "components/Common/Tags";
import WorkflowDropdown from "./WorkflowDropdown";
import UtcDate from "components/UtcDate";
@@ -1,7 +1,7 @@
<template>
<markdown :markdown-config="markdownConfig" v-if="!edit"></markdown>
<markdown-editor
:initialMarkdown="markdownConfig.invocation_markdown"
:initial-markdown="markdownConfig.invocation_markdown"
:onupdate="onupdate"
v-else
></markdown-editor>
@@ -4,7 +4,7 @@
<b>Workflow Invocation State</b>
<span v-if="createdTime"> (invoked at {{ createdTime }})</span>
</span>
<div v-bind:class="{ 'context-wrapped': provideContext }">
<div :class="{ 'context-wrapped': provideContext }">
<div>
Step Scheduling
<span
@@ -15,25 +15,25 @@
@click="cancelWorkflowScheduling"
></span>
<div v-if="!stepCount">
<progress-bar note="Loading step state summary" :loading="true" :infoProgress="1" />
<progress-bar note="Loading step state summary" :loading="true" :info-progress="1" />
</div>
<div v-else-if="invocationState == 'cancelled'">
<progress-bar
note="Invocation scheduling cancelled - expected jobs and outputs may not be generated"
:errorProgress="1"
:error-progress="1"
/>
</div>
<div v-else-if="invocationState == 'failed'">
<progress-bar
note="Invocation scheduling failed - Galaxy administrator may have additional details in logs"
:errorProgress="1"
:error-progress="1"
/>
</div>
<div v-else>
<progress-bar
:note="stepStatesStr"
:okProgress="stepScheduledPercent"
:newProgress="stepOtherPercent"
:ok-progress="stepScheduledPercent"
:new-progress="stepOtherPercent"
/>
</div>
</div>
@@ -42,22 +42,22 @@
<div v-if="jobCount">
<progress-bar
:note="jobStatesStr"
:okProgress="okPercent"
:runningProgress="runningPercent"
:newProgress="otherPercent"
:errorProgress="errorPercent"
:okMessage="okMessage"
:runningMessage="runningMessage"
:errorMessage="errorMessage"
:ok-progress="okPercent"
:running-progress="runningPercent"
:new-progress="otherPercent"
:error-progress="errorPercent"
:ok-message="okMessage"
:running-message="runningMessage"
:error-message="errorMessage"
/>
</div>
<div v-else>
<progress-bar note="Loading job summary" :loading="true" :infoProgress="1" />
<progress-bar note="Loading job summary" :loading="true" :info-progress="1" />
</div>
</div>
<span v-if="invocationSchedulingTerminal && jobStatesTerminal">
<a v-bind:href="invocationLink">View Invocation Report</a>
<a class="fa fa-print" v-bind:href="invocationPdfLink"></a>
<a :href="invocationLink">View Invocation Report</a>
<a class="fa fa-print" :href="invocationPdfLink"></a>
</span>
</div>
</div>
@@ -1,10 +1,10 @@
<template>
<invocations
:invocationItems="invocationItems"
:invocation-items="invocationItems"
:loading="loading"
headerMessage="Workflow invocations that are still being scheduled are displayed on this page."
noInvocationsMessage="There are no scheduling workflow invocations to show currently."
:ownerGrid="false"
header-message="Workflow invocations that are still being scheduled are displayed on this page."
no-invocations-message="There are no scheduling workflow invocations to show currently."
:owner-grid="false"
>
</invocations>
</template>
@@ -1,8 +1,6 @@
<template>
<base-grid :is-loaded="isLoaded" :columns="columns" id="data-tables-grid">
<template v-slot:title>
Current data table registry contains {{ rows.length }} data tables
</template>
<template v-slot:title> Current data table registry contains {{ rows.length }} data tables </template>
<template slot:rows v-for="(row, index) in rows">
<tr :key="row.id" :class="[index % 2 === 0 ? 'tr' : 'odd_row']">
<td>
@@ -5,7 +5,7 @@
v-if="status !== 'error'"
:columns="columns"
:rows="dataTypes"
:isLoaded="isDataLoaded"
:is-loaded="isDataLoaded"
id="data-types-grid"
>
<template v-slot:title>
@@ -102,7 +102,7 @@ import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import DependencyIndexMixin from "./DependencyIndexMixin";
import ContainerResolutionDetails from "./ContainerResolutionDetails";
import { getContainerResolutionToolbox, resolveContainersWithInstall } from "../AdminServices.js";
import { getContainerResolutionToolbox, resolveContainersWithInstall } from "../AdminServices";
import { DESCRIPTION } from "./ContainerResolver";
Vue.use(BootstrapVue);
@@ -1,11 +1,9 @@
<template>
<div>
<b-nav tabs>
<b-nav-item v-bind:active="mode == 'dependencies'" @click="setMode('dependencies')"
>Dependencies</b-nav-item
>
<b-nav-item v-bind:active="mode == 'containers'" @click="setMode('containers')">Containers</b-nav-item>
<b-nav-item v-bind:active="mode == 'unused'" @click="setMode('unused')">Unused</b-nav-item>
<b-nav-item :active="mode == 'dependencies'" @click="setMode('dependencies')">Dependencies</b-nav-item>
<b-nav-item :active="mode == 'containers'" @click="setMode('containers')">Containers</b-nav-item>
<b-nav-item :active="mode == 'unused'" @click="setMode('unused')">Unused</b-nav-item>
</b-nav>
<resolution-index v-if="mode == 'dependencies'" />
<container-index v-else-if="mode == 'containers'" />
@@ -85,7 +85,7 @@ import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import DependencyIndexMixin from "./DependencyIndexMixin";
import ResolutionDetails from "./ResolutionDetails";
import { getToolboxDependencies, installDependencies, uninstallDependencies } from "../AdminServices.js";
import { getToolboxDependencies, installDependencies, uninstallDependencies } from "../AdminServices";
Vue.use(BootstrapVue);
@@ -22,7 +22,7 @@
</template>
<script>
import DependencyIndexWrapper from "./DependencyIndexWrapper";
import { getDependencyUnusedPaths, deletedUnusedPaths } from "../AdminServices.js";
import { getDependencyUnusedPaths, deletedUnusedPaths } from "../AdminServices";
export default {
components: { DependencyIndexWrapper },
@@ -11,7 +11,7 @@
</template>
<script>
import BaseList from "./BaseList";
import { getDisplayApplications, reloadDisplayApplications } from "./AdminServices.js";
import { getDisplayApplications, reloadDisplayApplications } from "./AdminServices";
export default {
components: {
@@ -6,7 +6,7 @@
</div>
</template>
<script>
import { getErrorStack } from "./AdminServices.js";
import { getErrorStack } from "./AdminServices";
export default {
data() {
@@ -3,9 +3,9 @@
<h5>Command Line</h5>
<pre class="text-white bg-dark"><code class="break-word">{{ commandLine }}</code></pre>
<h5>Job Parameters</h5>
<job-parameters :jobId="jobId" :includeTitle="false" />
<job-parameters :job-id="jobId" :include-title="false" />
<h5>Job Metrics</h5>
<job-metrics :jobId="jobId" :includeTitle="false" />
<job-metrics :job-id="jobId" :include-title="false" />
</b-card>
</template>
@@ -112,7 +112,7 @@
Unfinished Jobs: These jobs are unfinished and have had their state updated in the previous
{{ cutoffDisplay }} seconds.
</template>
<template v-slot:head(selected)="{ rowSelected }">
<template v-slot:head(selected)>
<b-form-checkbox
v-model="allSelected"
:indeterminate="indeterminate"
@@ -128,12 +128,12 @@
></b-form-checkbox>
</template>
<template v-slot:cell(job_info)="data">
<b-link :href="data.value.info_url" v-on:click.prevent="clickJobInfo(data.value.id)">
<b-link :href="data.value.info_url" @click.prevent="clickJobInfo(data.value.id)">
{{ data.value.id }}
</b-link>
</template>
<template v-slot:row-details="row">
<job-details :commandLine="row.item.command_line" :jobId="row.item.jobId" />
<job-details :command-line="row.item.command_line" :job-id="row.item.jobId" />
</template>
</b-table>
<b-alert v-if="!recentJobsItemsComputed.length" variant="secondary" show>
@@ -155,7 +155,7 @@
Recent Jobs: These jobs have completed in the previous {{ cutoffDisplay }} seconds.
</template>
<template v-slot:cell(job_info)="data">
<b-link :href="data.value.info_url" v-on:click.prevent="clickJobInfo(data.value.id)">
<b-link :href="data.value.info_url" @click.prevent="clickJobInfo(data.value.id)">
{{ data.value.id }}
</b-link>
</template>
@@ -163,7 +163,7 @@
<utc-date :date="data.value" mode="elapsed" />
</template>
<template v-slot:row-details="row">
<job-details :commandLine="row.item.command_line" :jobId="row.item.id" />
<job-details :command-line="row.item.command_line" :job-id="row.item.id" />
</template>
</b-table>
<b-modal ref="job-info-modal" scrollable hide-header ok-only @hidden="resetModalContents">
@@ -11,7 +11,7 @@
</template>
<script>
import BaseList from "./BaseList";
import { getInstalledRepositories, resetRepositoryMetadata } from "./AdminServices.js";
import { getInstalledRepositories, resetRepositoryMetadata } from "./AdminServices";
export default {
components: {
@@ -41,12 +41,12 @@
<div v-for="idp in oidc_idps" :key="idp" style="margin:0.5em">
<span v-if="oidc_idps_icons[idp]">
<b-button variant="link" class="d-block mt-3" @click="submitOIDCLogin(idp)">
<img v-bind:src="oidc_idps_icons[idp]" height="45" v-bind:alt="idp" />
<img :src="oidc_idps_icons[idp]" height="45" :alt="idp" />
</b-button>
</span>
<span v-else>
<b-button class="d-block mt-3" @click="submitOIDCLogin(idp)">
<i v-bind:class="oidc_idps[idp]" /> Sign in with
<i :class="oidc_idps[idp]" /> Sign in with
{{ idp.charAt(0).toUpperCase() + idp.slice(1) }}
</b-button>
</span>
+1 -1
View File
@@ -6,7 +6,7 @@
</h4>
</template>
<table>
<tr v-for="(xref, index) in xrefs" v-bind:key="index">
<tr v-for="(xref, index) in xrefs" :key="index">
<th>- {{ xref.reftype }}:</th>
<td>
<template v-if="xref.reftype == 'bio.tools'">
@@ -1,11 +1,12 @@
import $ from "jquery";
import Backbone from "backbone";
import Upload from "mvc/upload/upload-view";
import UploadModal from "components/Upload/UploadModal";
import _l from "utils/localization";
import { getGalaxyInstance } from "app";
import { getAppRoot } from "onload";
import Vue from "vue";
import ToolBox from "../../components/Panels/ToolBox.vue";
import SidePanel from "../../components/Panels/SidePanel.vue";
import ToolBox from "../../components/Panels/ToolBox";
import SidePanel from "../../components/Panels/SidePanel";
import { mountVueComponent } from "../../utils/mountVueComponent";
const ToolPanel = Backbone.View.extend({
@@ -13,14 +14,23 @@ const ToolPanel = Backbone.View.extend({
const Galaxy = getGalaxyInstance();
const appRoot = getAppRoot();
// add uploader button to Galaxy object
Galaxy.upload = new Upload({
upload_path: Galaxy.config.nginx_upload_path || `${appRoot}api/tools`,
chunk_upload_size: Galaxy.config.chunk_upload_size,
ftp_upload_site: Galaxy.config.ftp_upload_site,
default_genome: Galaxy.config.default_genome,
default_extension: Galaxy.config.default_extension
});
// add upload modal
const modalInstance = Vue.extend(UploadModal);
const propsData = {
uploadPath: Galaxy.config.nginx_upload_path || `${appRoot}api/tools`,
chunkUploadSize: Galaxy.config.chunk_upload_size,
ftpUploadSite: Galaxy.config.ftp_upload_site,
defaultGenome: Galaxy.config.default_genome,
defaultExtension: Galaxy.config.default_extension
};
const vm = document.createElement("div");
$("body").append(vm);
const upload = new modalInstance({
propsData: propsData
}).$mount(vm);
// attach upload entrypoint to Galaxy object
Galaxy.upload = upload;
// components for panel definition
this.model = new Backbone.Model({
+20 -6
View File
@@ -14,12 +14,26 @@ function logoutClick() {
const galaxy = getGalaxyInstance();
const session_csrf_token = galaxy.session_csrf_token;
const url = `${galaxy.root}user/logout?session_csrf_token=${session_csrf_token}`;
axios.get(url).then(() => {
if (galaxy.user) {
galaxy.user.clearSessionStorage();
}
window.top.location.href = `${galaxy.root}login?logout=true`;
});
axios
.get(url)
.then(() => {
if (galaxy.user) {
galaxy.user.clearSessionStorage();
}
// Check if we need to logout of OIDC IDP
if (galaxy.config.enable_oidc) {
return axios.get(`${galaxy.root}authnz/logout`);
} else {
return {};
}
})
.then(response => {
if (response.data && response.data.redirect_uri) {
window.top.location.href = response.data.redirect_uri;
} else {
window.top.location.href = `${galaxy.root}root/login?is_logout_redirect=true`;
}
});
}
const Collection = Backbone.Collection.extend({
+1 -1
View File
@@ -198,7 +198,7 @@ var SearchableModelMixin = {
var attrVal = this.get(attrKey);
//this.debug( 'searchAttribute', attrKey, attrVal, searchFor );
// bail if empty searchFor or unsearchable values
if (!searchFor || (attrVal === undefined || attrVal === null)) {
if (!searchFor || attrVal === undefined || attrVal === null) {
return false;
}
// pass to sep. fn for deep search of array attributes
@@ -195,7 +195,10 @@ export default Backbone.Model.extend({
_fieldBoolean: function(input_def) {
return new Ui.RadioButton.View({
id: `field-${input_def.id}`,
data: [{ label: "Yes", value: "true" }, { label: "No", value: "false" }],
data: [
{ label: "Yes", value: "true" },
{ label: "No", value: "false" }
],
onchange: input_def.onchange
});
},
@@ -22,7 +22,7 @@ export default Backbone.View.extend({
/** Checks if the limit has been reached */
_done: function() {
var done = _.size(this.content_list) > this.max;
var done = _.size(this.content_list) >= this.max;
this.$message[done ? "show" : "hide"]();
return done;
},
+8 -2
View File
@@ -176,7 +176,10 @@ const View = Backbone.View.extend({
display: "radio",
ignore: "__ignore__",
value: "__ignore__",
options: [["Yes", options.job_id], ["No", "__ignore__"]],
options: [
["Yes", options.job_id],
["No", "__ignore__"]
],
help: help
});
}
@@ -199,7 +202,10 @@ const View = Backbone.View.extend({
display: "radio",
ignore: "__ignore__",
value: "__ignore__",
options: [["No", false], ["Yes", true]]
options: [
["No", false],
["Yes", true]
]
});
}
},
@@ -77,9 +77,12 @@ var View = Backbone.View.extend({
saveRulesFn: rules => this._handleRulesSave(rules),
initialRules: value
};
ListCollectionCreator.ruleBasedCollectionCreatorModal(elements, elementsType, importType, options).done(
() => {}
);
ListCollectionCreator.ruleBasedCollectionCreatorModal(
elements,
elementsType,
importType,
options
).done(() => {});
},
_handleRulesSave: function(rules) {
@@ -211,18 +211,17 @@ var View = Backbone.View.extend({
return `
${_.escape(result.text)}
<div>
${_.reduce(
filteredTags.slice(0, 5),
(memo, tag) => {
const tagColors = keyedColorScheme(tag.slice(5));
return `${memo}&nbsp;<div style="background-color: ${tagColors.primary}; color: ${
tagColors.contrasting
}; border: 1px solid ${
tagColors.darker
}" class="badge badge-primary badge-tags">${_.escape(tag)}</div>`;
},
""
)}
${filteredTags.slice(0, 5).reduce((memo, tag) => {
const tagColors = keyedColorScheme(tag);
const isNametag = tag.indexOf("name:") === 0;
const tagDisplayText = isNametag ? `#${tag.slice(5)}` : tag;
const styleBlock = isNametag
? `style="background-color: ${tagColors.primary}; color: ${tagColors.contrasting}; border: 1px solid ${tagColors.darker}"`
: "";
return `${memo}&nbsp;<div ${styleBlock} class="badge badge-primary badge-tags">${_.escape(
tagDisplayText
)}</div>`;
}, "")}
${extraTagWarning}
</div>`;
}
@@ -1,21 +1,9 @@
/** Renders the collection uploader rows */
import _l from "utils/localization";
import $ from "jquery";
import _ from "underscore";
import Backbone from "backbone";
import Utils from "utils/utils";
import UploadSettings from "mvc/upload/upload-settings";
import Popover from "mvc/ui/ui-popover";
export default Backbone.View.extend({
/** Dictionary of upload states and associated icons */
status_classes: {
init: "upload-icon-button fa fa-trash-o",
queued: "upload-icon fa fa-spinner fa-spin",
running: "upload-icon fa fa-spinner fa-spin",
success: "upload-icon-button fa fa-check",
error: "upload-icon-button fa fa-exclamation-triangle"
},
import UploadBoxRow from "mvc/upload/uploadbox-row";
export default UploadBoxRow.extend({
initialize: function(app, options) {
var self = this;
this.app = app;
@@ -32,17 +20,7 @@ export default Backbone.View.extend({
this.$progress_bar = this.$(".upload-progress-bar");
this.$percentage = this.$(".upload-percentage");
// append popup to settings icon
this.settings = new Popover({
title: _l("Upload configuration"),
container: this.$(".upload-settings"),
placement: "bottom"
});
// identify default genome and extension values
// TODO: These appear unused.
//var default_genome = this.app.select_genome.value();
//var default_extension = this.app.select_extension.value();
this._setupSettings();
// handle click event
this.$symbol.on("click", () => {
@@ -58,18 +36,7 @@ export default Backbone.View.extend({
});
// model events
this.listenTo(this.model, "change:percentage", () => {
self._refreshPercentage();
});
this.listenTo(this.model, "change:status", () => {
self._refreshStatus();
});
this.listenTo(this.model, "change:info", () => {
self._refreshInfo();
});
this.listenTo(this.model, "change:file_size", () => {
self._refreshFileSize();
});
this._setupUploadBoxListeners();
this.listenTo(this.model, "remove", () => {
self.remove();
});
@@ -102,23 +69,6 @@ export default Backbone.View.extend({
}
},
/** Refresh info text */
_refreshInfo: function() {
var info = this.model.get("info");
if (info) {
this.$info_text.html(`<strong>Failed: </strong>${info}`).show();
} else {
this.$info_text.hide();
}
},
/** Refresh percentage status */
_refreshPercentage: function() {
var percentage = parseInt(this.model.get("percentage"));
this.$progress_bar.css({ width: `${percentage}%` });
this.$percentage.html(percentage != 100 ? `${percentage}%` : "Adding to history...");
},
/** Refresh status */
_refreshStatus: function() {
var status = this.model.get("status");
@@ -129,31 +79,7 @@ export default Backbone.View.extend({
this.model.set("enabled", status == "init");
var enabled = this.model.get("enabled");
this.$text_content.attr("disabled", !enabled);
if (status == "success") {
this.$el.addClass("table-success");
this.$percentage.html("100%");
}
if (status == "error") {
this.$el.addClass("table-danger");
this.$info_progress.hide();
}
},
/** Refresh file size */
_refreshFileSize: function() {
this.$size.html(Utils.bytesToString(this.model.get("file_size")));
},
/** Remove row */
_removeRow: function() {
if (["init", "success", "error"].indexOf(this.model.get("status")) !== -1) {
this.app.collection.remove(this.model);
}
},
/** Attach file info popup */
_showSettings: function() {
this.settings.show(new UploadSettings(this).$el);
this._renderStatusType(status);
},
/** View template */
@@ -1,497 +0,0 @@
/** Renders contents of the collection uploader */
import _l from "utils/localization";
import _ from "underscore";
import $ from "jquery";
import Backbone from "backbone";
import { getGalaxyInstance } from "app";
import UploadModel from "mvc/upload/upload-model";
import UploadRow from "mvc/upload/collection/collection-row";
import UploadFtp from "mvc/upload/upload-ftp";
import UploadExtension from "mvc/upload/upload-extension";
import Popover from "mvc/ui/ui-popover";
import Select from "mvc/ui/ui-select";
import Ui from "mvc/ui/ui-misc";
import "utils/uploadbox";
export default Backbone.View.extend({
// current upload size in bytes
upload_size: 0,
// contains upload row models
collection: new UploadModel.Collection(),
// keeps track of the current uploader state
counter: {
announce: 0,
success: 0,
error: 0,
running: 0,
reset: function() {
this.announce = this.success = this.error = this.running = 0;
}
},
initialize: function(app) {
var self = this;
this.app = app;
this.options = app.options;
this.list_extensions = app.list_extensions;
this.list_genomes = app.list_genomes;
this.ui_button = app.ui_button;
this.ftp_upload_site = app.currentFtp();
this.setElement(this._template());
// append buttons to dom
this.btnLocal = new Ui.Button({
id: "btn-local",
title: _l("Choose local files"),
onclick: function() {
self.uploadbox.select();
},
icon: "fa fa-laptop"
});
this.btnFtp = new Ui.Button({
id: "btn-ftp",
title: _l("Choose FTP files"),
onclick: function() {
self._eventFtp();
},
icon: "fa fa-folder-open-o"
});
this.btnCreate = new Ui.Button({
id: "btn-new",
title: "Paste/Fetch data",
onclick: function() {
self._eventCreate();
},
icon: "fa fa-edit"
});
this.btnStart = new Ui.Button({
id: "btn-start",
title: _l("Start"),
onclick: function() {
self._eventStart();
}
});
this.btnBuild = new Ui.Button({
id: "btn-build",
title: _l("Build"),
onclick: function() {
self._eventBuild();
}
});
this.btnStop = new Ui.Button({
id: "btn-stop",
title: _l("Pause"),
onclick: function() {
self._eventStop();
}
});
this.btnReset = new Ui.Button({
id: "btn-reset",
title: _l("Reset"),
onclick: function() {
self._eventReset();
}
});
this.btnClose = new Ui.Button({
id: "btn-close",
title: _l("Close"),
onclick: function() {
self.app.modal.hide();
}
});
_.each(
[
this.btnLocal,
this.btnFtp,
this.btnCreate,
this.btnStop,
this.btnReset,
this.btnStart,
this.btnBuild,
this.btnClose
],
button => {
self.$(".upload-buttons").prepend(button.$el);
}
);
// file upload
this.uploadbox = this.$(".upload-box").uploadbox({
url: this.app.options.upload_path,
announce: function(index, file) {
self._eventAnnounce(index, file);
},
initialize: function(index) {
return self.app.toData([self.collection.get(index)], self.history_id);
},
progress: function(index, percentage) {
self._eventProgress(index, percentage);
},
success: function(index, message) {
self._eventSuccess(index, message);
},
error: function(index, message) {
self._eventError(index, message);
},
complete: function() {
self._eventComplete();
},
ondragover: function() {
self.$(".upload-box").addClass("highlight");
},
ondragleave: function() {
self.$(".upload-box").removeClass("highlight");
}
});
// add ftp file viewer
this.ftp = new Popover({
title: _l("FTP files"),
container: this.btnFtp.$el
});
// select extension
this.select_extension = new Select.View({
container: this.$(".upload-footer-extension"),
data: _.filter(this.list_extensions, ext => !ext.composite_files),
value: this.options.default_extension,
onchange: function(extension) {
self.updateExtension(extension);
}
});
this.collectionType = "list";
this.select_collection = new Select.View({
container: this.$(".upload-footer-collection-type"),
data: [
{ id: "list", text: "List" },
{ id: "paired", text: "Paired" },
{ id: "list:paired", text: "List of Pairs" }
],
value: "list",
onchange: function(collectionType) {
self.updateCollectionType(collectionType);
}
});
// handle extension info popover
this.$(".upload-footer-extension-info")
.on("click", e => {
new UploadExtension({
$el: $(e.target),
title: self.select_extension.text(),
extension: self.select_extension.value(),
list: self.list_extensions,
placement: "top"
});
})
.on("mousedown", e => {
e.preventDefault();
});
// genome extension
this.select_genome = new Select.View({
css: "upload-footer-selection",
container: this.$(".upload-footer-genome"),
data: this.list_genomes,
value: this.options.default_genome,
onchange: function(genome) {
self.updateGenome(genome);
}
});
// events
this.collection.on("remove", model => {
self._eventRemove(model);
});
this._updateScreen();
},
/** A new file has been dropped/selected through the uploadbox plugin */
_eventAnnounce: function(index, file) {
this.counter.announce++;
var new_model = new UploadModel.Model({
id: index,
file_name: file.name,
file_size: file.size,
file_mode: file.mode || "local",
file_path: file.path,
file_data: file,
extension: this.select_extension.value(),
genome: this.select_genome.value()
});
this.collection.add(new_model);
var upload_row = new UploadRow(this, { model: new_model });
this.$(".upload-table > tbody:first").append(upload_row.$el);
this._updateScreen();
upload_row.render();
},
/** Progress */
_eventProgress: function(index, percentage) {
var it = this.collection.get(index);
it.set("percentage", percentage);
this.ui_button.model.set("percentage", this._uploadPercentage(percentage, it.get("file_size")));
},
/** Success */
_eventSuccess: function(index, message) {
// var hdaId = message["outputs"][0]["id"];
var hids = _.pluck(message["outputs"], "hid");
var it = this.collection.get(index);
it.set({ percentage: 100, status: "success", hids: hids });
this.ui_button.model.set("percentage", this._uploadPercentage(100, it.get("file_size")));
this.upload_completed += it.get("file_size") * 100;
this.counter.announce--;
this.counter.success++;
this._updateScreen();
const Galaxy = getGalaxyInstance();
Galaxy.currHistoryPanel.refreshContents();
},
/** Error */
_eventError: function(index, message) {
var it = this.collection.get(index);
it.set({ percentage: 100, status: "error", info: message });
this.ui_button.model.set({
percentage: this._uploadPercentage(100, it.get("file_size")),
status: "danger"
});
this.upload_completed += it.get("file_size") * 100;
this.counter.announce--;
this.counter.error++;
this._updateScreen();
},
/** Queue is done */
_eventComplete: function() {
this.collection.each(model => {
model.get("status") == "queued" && model.set("status", "init");
});
this.counter.running = 0;
this._updateScreen();
},
_eventBuild: function() {
const Galaxy = getGalaxyInstance();
var allHids = [];
_.forEach(this.collection.models, upload => {
allHids.push.apply(allHids, upload.get("hids"));
});
var models = _.map(allHids, hid => Galaxy.currHistoryPanel.collection.getByHid(hid));
var selection = new Galaxy.currHistoryPanel.collection.constructor(models);
// I'm building the selection wrong because I need to set this historyId directly.
selection.historyId = Galaxy.currHistoryPanel.collection.historyId;
Galaxy.currHistoryPanel.buildCollection(this.collectionType, selection, true);
this.counter.running = 0;
this._updateScreen();
this._eventReset();
this.app.modal.hide();
},
/** Remove model from upload list */
_eventRemove: function(model) {
var status = model.get("status");
if (status == "success") {
this.counter.success--;
} else if (status == "error") {
this.counter.error--;
} else {
this.counter.announce--;
}
this.uploadbox.remove(model.id);
this._updateScreen();
},
//
// events triggered by this view
//
/** Show/hide ftp popup */
_eventFtp: function() {
var self = this;
this.ftp.show(
new UploadFtp({
collection: this.collection,
ftp_upload_site: this.ftp_upload_site,
onadd: function(ftp_file) {
return self.uploadbox.add([
{
mode: "ftp",
name: ftp_file.path,
size: ftp_file.size,
path: ftp_file.path
}
]);
},
onremove: function(model_index) {
self.collection.remove(model_index);
}
}).$el
);
},
/** Create a new file */
_eventCreate: function() {
this.uploadbox.add([{ name: "New File", size: 0, mode: "new" }]);
},
/** Start upload process */
_eventStart: function() {
if (this.counter.announce == 0 || this.counter.running > 0) {
return;
}
var self = this;
this.upload_size = 0;
this.upload_completed = 0;
this.collection.each(model => {
if (model.get("status") == "init") {
model.set("status", "queued");
self.upload_size += model.get("file_size");
}
});
this.ui_button.model.set({ percentage: 0, status: "success" });
this.counter.running = this.counter.announce;
this.history_id = this.app.currentHistory();
this.uploadbox.start();
this._updateScreen();
},
/** Pause upload process */
_eventStop: function() {
if (this.counter.running > 0) {
this.ui_button.model.set("status", "info");
$(".upload-top-info").html("Queue will pause after completing the current file...");
this.uploadbox.stop();
}
},
/** Remove all */
_eventReset: function() {
if (this.counter.running == 0) {
this.collection.reset();
this.counter.reset();
this.uploadbox.reset();
this.select_extension.value(this.options.default_extension);
this.select_genome.value(this.options.default_genome);
this.ui_button.model.set("percentage", 0);
this._updateScreen();
}
},
/** Update extension for all models */
updateExtension: function(extension, defaults_only) {
var self = this;
this.collection.each(model => {
if (
model.get("status") == "init" &&
(model.get("extension") == self.options.default_extension || !defaults_only)
) {
model.set("extension", extension);
}
});
},
/** Update collection type */
updateCollectionType: function(collectionType) {
this.collectionType = collectionType;
},
/** Update genome for all models */
updateGenome: function(genome, defaults_only) {
var self = this;
this.collection.each(model => {
if (
model.get("status") == "init" &&
(model.get("genome") == self.options.default_genome || !defaults_only)
) {
model.set("genome", genome);
}
});
},
/** Set screen */
_updateScreen: function() {
var message = "";
if (this.counter.announce == 0) {
if (this.uploadbox.compatible()) {
message = "&nbsp;";
} else {
message =
"Browser does not support Drag & Drop. Try Firefox 4+, Chrome 7+, IE 10+, Opera 12+ or Safari 6+.";
}
} else {
if (this.counter.running == 0) {
message = `You added ${this.counter.announce} file(s) to the queue. Add more files or click 'Start' to proceed.`;
} else {
message = `Please wait...${this.counter.announce} out of ${this.counter.running} remaining.`;
}
}
this.$(".upload-top-info").html(message);
var enable_reset =
this.counter.running == 0 && this.counter.announce + this.counter.success + this.counter.error > 0;
var enable_start = this.counter.running == 0 && this.counter.announce > 0;
var enable_build =
this.counter.running == 0 &&
this.counter.announce == 0 &&
this.counter.success > 0 &&
this.counter.error == 0;
var enable_sources = this.counter.running == 0;
var show_table = this.counter.announce + this.counter.success + this.counter.error > 0;
this.btnReset[enable_reset ? "enable" : "disable"]();
this.btnStart[enable_start ? "enable" : "disable"]();
this.btnStart.$el[enable_start ? "addClass" : "removeClass"]("btn-primary");
this.btnBuild[enable_build ? "enable" : "disable"]();
this.btnBuild.$el[enable_build ? "addClass" : "removeClass"]("btn-primary");
this.btnStop[this.counter.running > 0 ? "enable" : "disable"]();
this.btnLocal[enable_sources ? "enable" : "disable"]();
this.btnFtp[enable_sources ? "enable" : "disable"]();
this.btnCreate[enable_sources ? "enable" : "disable"]();
this.btnFtp.$el[this.ftp_upload_site ? "show" : "hide"]();
this.$(".upload-table")[show_table ? "show" : "hide"]();
this.$(".upload-helper")[show_table ? "hide" : "show"]();
},
/** Calculate percentage of all queued uploads */
_uploadPercentage: function(percentage, size) {
return (this.upload_completed + percentage * size) / this.upload_size;
},
/** Template */
_template: function() {
return `<div class="upload-view-default">
<div class="upload-top">
<div class="upload-top-info"/>
</div>
<div class="upload-box">
<div class="upload-helper">
<i class="fa fa-files-o"/>Drop files here
</div>
<table class="upload-table ui-table-striped" style="display: none;">
<thead>
<tr>
<th>Name</th>
<th>Size</th>
<th>Status</th>
<th/>
</tr>
</thead>
<tbody/>
</table>
</div>
<div class="upload-footer">
<span class="upload-footer-title">Collection Type:</span>
<span class="upload-footer-collection-type"/>
<span class="upload-footer-title">File Type:</span>
<span class="upload-footer-extension"/>
<span class="upload-footer-extension-info upload-icon-button fa fa-search"/>
<span class="upload-footer-title">Genome (set all):</span>
<span class="upload-footer-genome"/>
</div>
<div class="upload-buttons"/>
</div>`;
}
});
@@ -1,217 +0,0 @@
import _ from "underscore";
import Backbone from "backbone";
import { getAppRoot } from "onload/loadConfig";
import { getGalaxyInstance } from "app";
import _l from "utils/localization";
import Ui from "mvc/ui/ui-misc";
import Select from "mvc/ui/ui-select";
import UploadUtils from "mvc/upload/upload-utils";
import axios from "axios";
export default Backbone.View.extend({
initialize: function(app) {
this.app = app;
this.options = app.options;
this.ftpFiles = [];
this.ftpUploadSite = app.currentFtp();
this.setElement(this._template());
this.btnBuild = new Ui.Button({
id: "btn-build",
title: _l("Build"),
onclick: () => {
this._eventBuild();
}
});
this.btnBuild.$el.addClass("btn-primary");
this.btnReset = new Ui.Button({
id: "btn-reset",
title: _l("Reset"),
onclick: () => this._eventReset()
});
this.btnClose = new Ui.Button({
id: "btn-close",
title: _l("Close"),
onclick: () => this.app.modal.hide()
});
_.each([this.btnReset, this.btnBuild, this.btnClose], button => {
this.$(".upload-buttons").prepend(button.$el);
});
const dataTypeOptions = [{ id: "datasets", text: "Datasets" }, { id: "collections", text: "Collection(s)" }];
this.dataType = "datasets";
this.dataTypeView = new Select.View({
css: "upload-footer-selection",
container: this.$(".rule-data-type"),
data: dataTypeOptions,
value: this.dataType,
onchange: value => {
this.dataType = value;
// this._renderSelectedType();
}
});
const selectionTypeOptions = [
{ id: "paste", text: "Pasted Table" },
{ id: "dataset", text: "History Dataset" }
];
if (this.ftpUploadSite) {
selectionTypeOptions.push({ id: "ftp", text: "FTP Directory" });
}
this.selectionType = "paste";
this.selectionTypeView = new Select.View({
css: "upload-footer-selection",
container: this.$(".rule-select-type"),
data: selectionTypeOptions,
value: this.selectionType,
onchange: value => {
this.selectionType = value;
this._renderSelectedType();
}
});
this.selectedDatasetId = null;
this.$sourceContent = this.$(".upload-rule-source-content");
this.$sourceContent.on("change keyup paste", () => {
this._updateBuildState();
});
this._renderSelectedType();
},
_renderSelectedType: function() {
const selectionType = this.selectionType;
const Galaxy = getGalaxyInstance();
if (selectionType == "dataset") {
if (!this.datasetSelectorView) {
this.selectedDatasetId = null;
const history = Galaxy && Galaxy.currHistoryPanel && Galaxy.currHistoryPanel.model;
const historyContentModels = history.contents.models;
const options = [];
for (const historyContentModel of historyContentModels) {
const attr = historyContentModel.attributes;
if (attr.history_content_type !== "dataset") {
continue;
}
options.push({ id: attr.id, text: `${attr.hid}: ${_.escape(attr.name)}` });
}
this.datasetSelectorView = new Select.View({
container: this.$(".dataset-selector"),
data: options,
placeholder: _l("Select a dataset"),
onchange: val => {
this._onDataset(val);
}
});
} else {
this.datasetSelectorView.value(null);
}
} else if (selectionType == "ftp") {
UploadUtils.getRemoteFiles(ftp_files => {
this._setPreview(ftp_files.map(file => file["path"]).join("\n"));
this.ftpFiles = ftp_files;
});
}
this._updateScreen();
},
_onDataset: function(selectedDatasetId) {
const Galaxy = getGalaxyInstance();
this.selectedDatasetId = selectedDatasetId;
if (!selectedDatasetId) {
this._setPreview("");
return;
}
axios
.get(
`${getAppRoot()}api/histories/${Galaxy.currHistoryPanel.model.id}/contents/${selectedDatasetId}/display`
)
.then(response => {
this._setPreview(response.data);
})
.catch(error => console.log(error));
},
/** Remove all */
_eventReset: function() {
if (this.datasetSelectorView) {
this.datasetSelectorView.value(null);
}
this.$sourceContent.val("");
this._updateScreen();
},
_eventBuild: function() {
const selection = this.$sourceContent.val();
this._buildSelection(selection);
},
_buildSelection: function(content) {
const selectionType = this.selectionType;
const selection = {};
const Galaxy = getGalaxyInstance();
if (selectionType == "dataset" || selectionType == "paste") {
selection.selectionType = "raw";
selection.content = content;
} else if (selectionType == "ftp") {
selection.selectionType = "ftp";
selection.elements = this.ftpFiles;
selection.ftpUploadSite = this.ftpUploadSite;
}
selection.dataType = this.dataType;
Galaxy.currHistoryPanel.buildCollection("rules", selection, true);
this.app.modal.hide();
},
_setPreview: function(content) {
this.$sourceContent.val(content);
this._updateScreen();
},
_updateScreen: function() {
this._updateBuildState();
const selectionType = this.selectionType;
this.$("#upload-rule-dataset-option")[selectionType == "dataset" ? "show" : "hide"]();
this.$sourceContent.attr("disabled", selectionType !== "paste");
},
_updateBuildState: function() {
const selection = this.$sourceContent.val();
this.btnBuild[selection ? "enable" : "disable"]();
this.btnBuild.$el[selection ? "addClass" : "removeClass"]("btn-primary");
},
_template: function() {
return `
<div class="upload-view-default">
<div class="upload-top">
<div class="upload-top-info">
Tabular source data to extract collection files and metadata from
</div>
</div>
<div class="upload-box" style="height: 335px;">
<span style="width: 25%; display: inline; height: 100%" class="float-left">
<div class="upload-rule-option">
<div class="upload-rule-option-title">${_l("Upload data as")}:</div>
<div class="rule-data-type" />
</div>
<div class="upload-rule-option">
<div class="upload-rule-option-title">${_l("Load tabular data from")}:</div>
<div class="rule-select-type" />
</div>
<div id="upload-rule-dataset-option" class="upload-rule-option">
<div class="upload-rule-option-title">${_l("Select dataset to load")}:</div>
<div class="dataset-selector" />
</div>
</span>
<span style="display: inline; float: right; width: 75%; height: 300px">
<textarea class="upload-rule-source-content form-control" style="height: 100%"></textarea>
</span>
</div>
<div class="clear" />
<!--
<div class="upload-footer">
</div>
-->
<div class="upload-buttons"/>
</div>
`;
}
});
@@ -72,7 +72,7 @@ export default Backbone.View.extend({
self.uploadinput.dialog();
}
});
if (this.app.ftp_upload_site) {
if (this.app.ftpUploadSite) {
this.button_menu.addMenu({
icon: "fa-folder-open-o",
title: _l("Choose FTP file"),
@@ -261,7 +261,7 @@ export default Backbone.View.extend({
var self = this;
this.ftp.show(
new UploadFtp({
ftp_upload_site: this.app.ftp_upload_site,
ftp_upload_site: this.app.ftpUploadSite,
onchange: function(ftp_file) {
self.ftp.hide();
if (self.model.get("status") != "running" && ftp_file) {
@@ -1,231 +0,0 @@
/** Renders contents of the composite uploader */
import _l from "utils/localization";
import _ from "underscore";
import $ from "jquery";
import Backbone from "backbone";
import { getGalaxyInstance } from "app";
// import Utils from "utils/utils";
import UploadModel from "mvc/upload/upload-model";
import UploadRow from "mvc/upload/composite/composite-row";
import UploadExtension from "mvc/upload/upload-extension";
import Select from "mvc/ui/ui-select";
import Ui from "mvc/ui/ui-misc";
export default Backbone.View.extend({
collection: new UploadModel.Collection(),
initialize: function(app) {
var self = this;
this.app = app;
this.options = app.options;
this.list_extensions = app.list_extensions;
this.list_genomes = app.list_genomes;
this.ftp_upload_site = app.currentFtp();
this.setElement(this._template());
// create button section
this.btnReset = new Ui.Button({
id: "btn-reset",
title: _l("Reset"),
onclick: function() {
self._eventReset();
}
});
this.btnStart = new Ui.Button({
title: _l("Start"),
onclick: function() {
self._eventStart();
}
});
this.btnClose = new Ui.Button({
title: _l("Close"),
onclick: function() {
self.app.modal.hide();
}
});
// append buttons to dom
_.each([this.btnReset, this.btnStart, this.btnClose], button => {
self.$(".upload-buttons").prepend(button.$el);
});
// select extension
this.select_extension = new Select.View({
css: "upload-footer-selection",
container: this.$(".upload-footer-extension"),
data: _.filter(this.list_extensions, ext => ext.composite_files),
onchange: function(extension) {
self.collection.reset();
var details = _.findWhere(self.list_extensions, {
id: extension
});
if (details && details.composite_files) {
_.each(details.composite_files, item => {
self.collection.add({
id: self.collection.size(),
file_desc: item.description || item.name,
optional: item.optional
});
});
}
}
});
// handle extension info popover
this.$(".upload-footer-extension-info")
.on("click", e => {
new UploadExtension({
$el: $(e.target),
title: self.select_extension.text(),
extension: self.select_extension.value(),
list: self.list_extensions,
placement: "top"
});
})
.on("mousedown", e => {
e.preventDefault();
});
// genome extension
this.select_genome = new Select.View({
css: "upload-footer-selection",
container: this.$(".upload-footer-genome"),
data: this.list_genomes,
value: this.options.default_genome
});
// listener for collection triggers on change in composite datatype and extension selection
this.listenTo(this.collection, "add", model => {
self._eventAnnounce(model);
});
this.listenTo(this.collection, "change add", () => {
self.render();
});
this.select_extension.options.onchange(this.select_extension.value());
this.render();
},
render: function() {
var model = this.collection.first();
if (model && model.get("status") == "running") {
this.select_genome.disable();
this.select_extension.disable();
} else {
this.select_genome.enable();
this.select_extension.enable();
}
if (
this.collection.where({ status: "ready" }).length + this.collection.where({ optional: true }).length ==
this.collection.length &&
this.collection.length > 0
) {
this.btnStart.enable();
this.btnStart.$el.addClass("btn-primary");
} else {
this.btnStart.disable();
this.btnStart.$el.removeClass("btn-primary");
}
this.$(".upload-table")[this.collection.length > 0 ? "show" : "hide"]();
},
//
// upload events / process pipeline
//
/** Builds the basic ui with placeholder rows for each composite data type file */
_eventAnnounce: function(model) {
var upload_row = new UploadRow(this, { model: model });
this.$(".upload-table > tbody:first").append(upload_row.$el);
this.$(".upload-table")[this.collection.length > 0 ? "show" : "hide"]();
upload_row.render();
},
/** Start upload process */
_eventStart: function() {
var self = this;
this.collection.each(model => {
model.set({
genome: self.select_genome.value(),
extension: self.select_extension.value()
});
});
$.uploadpost({
url: this.app.options.upload_path,
data: this.app.toData(this.collection.filter()),
success: function(message) {
self._eventSuccess(message);
},
error: function(message) {
self._eventError(message);
},
progress: function(percentage) {
self._eventProgress(percentage);
}
});
},
/** Remove all */
_eventReset: function() {
if (this.collection.where({ status: "running" }).length == 0) {
this.collection.reset();
this.select_extension.value(this.options.default_extension);
this.select_genome.value(this.options.default_genome);
this.render();
}
},
/** Refresh progress state */
_eventProgress: function(percentage) {
this.collection.each(it => {
it.set("percentage", percentage);
});
},
/** Refresh success state */
_eventSuccess: function(message) {
const Galaxy = getGalaxyInstance();
this.collection.each(it => {
it.set("status", "success");
});
Galaxy.currHistoryPanel.refreshContents();
},
/** Refresh error state */
_eventError: function(message) {
this.collection.each(it => {
it.set({ status: "error", info: message });
});
},
/** Load html template */
_template: function() {
return `<div class="upload-view-composite">
<div class="upload-top">
<div class="upload-top-info">&nbsp;</div>
</div>
<div class="upload-box">
<table class="upload-table ui-table-striped" style="display: none;">
<thead>
<tr>
<th/>
<th/>
<th>Description</th>
<th>Name</th>
<th>Size</th>
<th>Settings</th>
<th>Status</th>
</tr>
</thead>
<tbody/>
</table>
</div>
<div class="upload-footer">
<span class="upload-footer-title">Composite Type:</span>
<span class="upload-footer-extension"/>
<span class="upload-footer-extension-info upload-icon-button fa fa-search"/>
<span class="upload-footer-title">Genome/Build:</span>
<span class="upload-footer-genome"/>
</div>
<div class="upload-buttons"/>
</div>`;
}
});
@@ -2,27 +2,15 @@
import $ from "jquery";
import _ from "underscore";
import Backbone from "backbone";
import _l from "utils/localization";
import Utils from "utils/utils";
import UploadSettings from "mvc/upload/upload-settings";
import Popover from "mvc/ui/ui-popover";
import UploadExtension from "mvc/upload/upload-extension";
import UploadBoxRow from "mvc/upload/uploadbox-row";
import Select from "mvc/ui/ui-select";
export default Backbone.View.extend({
/** Dictionary of upload states and associated icons */
status_classes: {
init: "upload-icon-button fa fa-trash-o",
queued: "upload-icon fa fa-spinner fa-spin",
running: "upload-icon fa fa-spinner fa-spin",
warning: "upload-icon fa fa-spinner fa-spin",
success: "upload-icon-button fa fa-check",
error: "upload-icon-button fa fa-exclamation-triangle"
},
export default UploadBoxRow.extend({
initialize: function(app, options) {
var self = this;
this.app = app;
this.list_extensions = app.list_extensions;
this.list_extensions = app.listExtensions;
this.model = options.model;
this.setElement(this._template(options.model));
this.$mode = this.$(".upload-mode");
@@ -37,21 +25,16 @@ export default Backbone.View.extend({
this.$progress_bar = this.$(".upload-progress-bar");
this.$percentage = this.$(".upload-percentage");
// append popup to settings icon
this.settings = new Popover({
title: _l("Upload configuration"),
container: this.$(".upload-settings"),
placement: "bottom"
});
this._setupSettings();
// identify default genome and extension values
var default_genome = this.app.select_genome.value();
var default_extension = this.app.select_extension.value();
var default_genome = this.app.genome;
var default_extension = this.app.extension;
// create select genomes
this.select_genome = new Select.View({
css: "upload-genome",
data: self.app.list_genomes,
data: self.app.listGenomes,
container: this.$(".upload-genome"),
value: default_genome,
onchange: function(genome) {
@@ -62,7 +45,7 @@ export default Backbone.View.extend({
// create select extension
this.select_extension = new Select.View({
css: "upload-extension",
data: _.filter(this.list_extensions, ext => !ext.composite_files),
data: self.app.extensions,
container: this.$(".upload-extension"),
value: default_extension,
onchange: function(extension) {
@@ -123,24 +106,13 @@ export default Backbone.View.extend({
});
// model events
this.listenTo(this.model, "change:percentage", () => {
self._refreshPercentage();
});
this.listenTo(this.model, "change:status", () => {
self._refreshStatus();
});
this.listenTo(this.model, "change:info", () => {
self._refreshInfo();
});
this._setupUploadBoxListeners();
this.listenTo(this.model, "change:genome", () => {
self._refreshGenome();
});
this.listenTo(this.model, "change:extension", () => {
self._refreshExtension();
});
this.listenTo(this.model, "change:file_size", () => {
self._refreshFileSize();
});
},
render: function() {
@@ -195,23 +167,6 @@ export default Backbone.View.extend({
this.select_genome.value(this.model.get("genome"));
},
/** Refresh info text */
_refreshInfo: function() {
var info = this.model.get("info");
if (info) {
this.$info_text.html(`<strong>Warning: </strong>${info}`).show();
} else {
this.$info_text.hide();
}
},
/** Refresh percentage status */
_refreshPercentage: function() {
var percentage = parseInt(this.model.get("percentage"));
this.$progress_bar.css({ width: `${percentage}%` });
this.$percentage.html(percentage != 100 ? `${percentage}%` : "Adding to history...");
},
/** Refresh status */
_refreshStatus: function() {
var status = this.model.get("status");
@@ -232,33 +187,7 @@ export default Backbone.View.extend({
}
this.$info_progress.show();
this.$el.removeClass().addClass("upload-row");
if (status == "success") {
this.$el.addClass("table-success");
this.$percentage.html("100%");
} else if (status == "error") {
this.$el.addClass("table-danger");
this.$info_progress.hide();
} else if (status == "warning") {
this.$el.addClass("table-warning");
this.$info_progress.hide();
}
},
/** Refresh file size */
_refreshFileSize: function() {
this.$size.html(Utils.bytesToString(this.model.get("file_size")));
},
/** Remove row */
_removeRow: function() {
if (["init", "success", "error"].indexOf(this.model.get("status")) !== -1) {
this.app.collection.remove(this.model);
}
},
/** Attach file info popup */
_showSettings: function() {
this.settings.show(new UploadSettings(this).$el);
this._renderStatusType(status);
},
/** Make extension popover */
@@ -1,499 +0,0 @@
/** Renders contents of the default uploader */
import $ from "jquery";
import Backbone from "backbone";
import _ from "underscore";
import _l from "utils/localization";
import UploadModel from "mvc/upload/upload-model";
import UploadRow from "mvc/upload/default/default-row";
import UploadFtp from "mvc/upload/upload-ftp";
import UploadExtension from "mvc/upload/upload-extension";
import Popover from "mvc/ui/ui-popover";
import Select from "mvc/ui/ui-select";
import Ui from "mvc/ui/ui-misc";
import LazyLimited from "mvc/lazy/lazy-limited";
import "utils/uploadbox";
import { getGalaxyInstance } from "app";
export default Backbone.View.extend({
// current upload size in bytes
upload_size: 0,
// contains upload row models
collection: new UploadModel.Collection(),
// keeps track of the current uploader state
counter: {
announce: 0,
success: 0,
error: 0,
running: 0,
reset: function() {
this.announce = this.success = this.error = this.running = 0;
}
},
initialize: function(app) {
var self = this;
this.app = app;
this.options = app.options;
this.list_extensions = app.list_extensions;
this.list_genomes = app.list_genomes;
this.ui_button = app.ui_button;
this.ftp_upload_site = app.currentFtp();
// build template
this.setElement(this._template());
this.$uploadbox = this.$(".upload-box");
this.$uploadtable = this.$(".upload-table");
// append buttons to dom
this.btnLocal = new Ui.Button({
id: "btn-local",
title: _l("Choose local file"),
onclick: function() {
self.uploadbox.select();
},
icon: "fa fa-laptop"
});
this.btnFtp = new Ui.Button({
id: "btn-ftp",
title: _l("Choose FTP file"),
onclick: function() {
self._eventFtp();
},
icon: "fa fa-folder-open-o"
});
this.btnCreate = new Ui.Button({
id: "btn-new",
title: "Paste/Fetch data",
onclick: function() {
self._eventCreate();
},
icon: "fa fa-edit"
});
this.btnStart = new Ui.Button({
id: "btn-start",
title: _l("Start"),
onclick: function() {
self._eventStart();
}
});
this.btnStop = new Ui.Button({
id: "btn-stop",
title: _l("Pause"),
onclick: function() {
self._eventStop();
}
});
this.btnReset = new Ui.Button({
id: "btn-reset",
title: _l("Reset"),
onclick: function() {
self._eventReset();
}
});
this.btnClose = new Ui.Button({
id: "btn-close",
title: _l("Close"),
onclick: function() {
self.app.modal.hide();
}
});
_.each(
[this.btnLocal, this.btnFtp, this.btnCreate, this.btnStop, this.btnReset, this.btnStart, this.btnClose],
button => {
self.$(".upload-buttons").prepend(button.$el);
}
);
// file upload
this.uploadbox = this.$uploadbox.uploadbox({
url: this.app.options.upload_path,
announce: function(index, file) {
self._eventAnnounce(index, file);
},
initialize: function(index) {
return self.app.toData([self.collection.get(index)], self.history_id);
},
progress: function(index, percentage) {
self._eventProgress(index, percentage);
},
success: function(index, message) {
self._eventSuccess(index, message);
},
error: function(index, message) {
self._eventError(index, message);
},
warning: function(index, message) {
self._eventWarning(index, message);
},
complete: function() {
self._eventComplete();
},
ondragover: function() {
self.$uploadbox.addClass("highlight");
},
ondragleave: function() {
self.$uploadbox.removeClass("highlight");
}
});
// add ftp file viewer
this.ftp = new Popover({
title: _l("FTP files"),
container: this.btnFtp.$el
});
// select extension
this.select_extension = new Select.View({
css: "upload-footer-selection",
container: this.$(".upload-footer-extension"),
data: _.filter(this.list_extensions, ext => !ext.composite_files),
value: this.options.default_extension,
onchange: function(extension) {
self._changeExtension(extension);
}
});
// handle extension info popover
this.$(".upload-footer-extension-info")
.on("click", e => {
new UploadExtension({
$el: $(e.target),
title: self.select_extension.text(),
extension: self.select_extension.value(),
list: self.list_extensions,
placement: "top"
});
})
.on("mousedown", e => {
e.preventDefault();
});
// genome extension
this.select_genome = new Select.View({
css: "upload-footer-selection",
container: this.$(".upload-footer-genome"),
data: this.list_genomes,
value: this.options.default_genome,
onchange: function(genome) {
self._changeGenome(genome);
}
});
// Lazy load helper
this.loader = new LazyLimited({
$container: this.$uploadbox,
collection: this.collection,
new_content: function(model) {
var upload_row = new UploadRow(self, { model: model });
self.$uploadtable.find("> tbody:first").append(upload_row.$el);
upload_row.render();
return upload_row;
}
});
// events
this.collection.on("remove", model => {
self._eventRemove(model);
});
this.render();
},
render: function() {
var message = "";
if (this.counter.announce === 0) {
if (this.uploadbox.compatible()) {
message = "&nbsp;";
} else {
message =
"Browser does not support Drag & Drop. Try Firefox 4+, Chrome 7+, IE 10+, Opera 12+ or Safari 6+.";
}
} else {
if (this.counter.running === 0) {
message = `You added ${this.counter.announce} file(s) to the queue. Add more files or click 'Start' to proceed.`;
} else {
message = `Please wait...${this.counter.announce} out of ${this.counter.running} remaining.`;
}
}
this.$(".upload-top-info").html(message);
var enable_reset =
this.counter.running === 0 && this.counter.announce + this.counter.success + this.counter.error > 0;
var enable_start = this.counter.running === 0 && this.counter.announce > 0;
var enable_sources = this.counter.running === 0;
var show_table = this.counter.announce + this.counter.success + this.counter.error > 0;
this.btnReset[enable_reset ? "enable" : "disable"]();
this.btnStart[enable_start ? "enable" : "disable"]();
this.btnStart.$el[enable_start ? "addClass" : "removeClass"]("btn-primary");
this.btnStop[this.counter.running > 0 ? "enable" : "disable"]();
this.btnLocal[enable_sources ? "enable" : "disable"]();
this.btnFtp[enable_sources ? "enable" : "disable"]();
this.btnCreate[enable_sources ? "enable" : "disable"]();
this.btnFtp.$el[this.ftp_upload_site ? "show" : "hide"]();
this.$(".upload-table")[show_table ? "show" : "hide"]();
this.$(".upload-helper")[show_table ? "hide" : "show"]();
},
/** A new file has been dropped/selected through the uploadbox plugin */
_eventAnnounce: function(index, file) {
this.counter.announce++;
var new_model = new UploadModel.Model({
id: index,
file_name: file.name,
file_size: file.size,
file_mode: file.mode || "local",
file_path: file.path,
file_data: file
});
this.render();
this.collection.add(new_model);
},
/** Progress */
_eventProgress: function(index, percentage) {
var it = this.collection.get(index);
it.set({ percentage: percentage, status: "running", info: "" });
this.ui_button.model.set("percentage", this._uploadPercentage(percentage, it.get("file_size")));
},
/** Success */
_eventSuccess: function(index, message) {
const Galaxy = getGalaxyInstance();
var it = this.collection.get(index);
it.set({ percentage: 100, status: "success" });
this.ui_button.model.set("percentage", this._uploadPercentage(100, it.get("file_size")));
this.upload_completed += it.get("file_size") * 100;
this.counter.announce--;
this.counter.success++;
this.render();
Galaxy.currHistoryPanel.refreshContents();
},
/** Warning */
_eventWarning: function(index, message) {
var it = this.collection.get(index);
it.set({ status: "warning", info: message });
},
/** Error */
_eventError: function(index, message) {
var it = this.collection.get(index);
it.set({ percentage: 100, status: "error", info: message });
this.ui_button.model.set({
percentage: this._uploadPercentage(100, it.get("file_size")),
status: "danger"
});
this.upload_completed += it.get("file_size") * 100;
this.counter.announce--;
this.counter.error++;
this.render();
},
/** Queue is done */
_eventComplete: function() {
this.collection.each(model => {
if (model.get("status") == "queued") {
model.set("status", "init");
}
});
this.counter.running = 0;
this.render();
},
/** Remove model from upload list */
_eventRemove: function(model) {
var status = model.get("status");
if (status == "success") {
this.counter.success--;
} else if (status == "error") {
this.counter.error--;
} else {
this.counter.announce--;
}
this.uploadbox.remove(model.id);
this.render();
},
//
// events triggered by this view
//
/** Show/hide ftp popup */
_eventFtp: function() {
var self = this;
this.ftp.show(
new UploadFtp({
collection: this.collection,
ftp_upload_site: this.ftp_upload_site,
onadd: function(ftp_file) {
return self.uploadbox.add([
{
mode: "ftp",
name: ftp_file.path,
size: ftp_file.size,
path: ftp_file.path
}
]);
},
onremove: function(model_index) {
self.collection.remove(model_index);
}
}).$el
);
},
/** Create a new file */
_eventCreate: function() {
this.uploadbox.add([{ name: "New File", size: 0, mode: "new" }]);
},
/** Start upload process */
_eventStart: function() {
if (this.counter.announce !== 0 && this.counter.running === 0) {
// prepare upload process
var self = this;
this.upload_size = 0;
this.upload_completed = 0;
this.collection.each(model => {
if (model.get("status") == "init") {
model.set("status", "queued");
self.upload_size += model.get("file_size");
}
});
this.ui_button.model.set({
percentage: 0,
status: "success"
});
this.counter.running = this.counter.announce;
this.history_id = this.app.currentHistory();
// package ftp files separately, and remove them from queue
this._uploadFtp();
// queue remaining files
const Galaxy = getGalaxyInstance();
this.uploadbox.start({
id: Galaxy.user.id,
chunk_upload_size: this.app.options.chunk_upload_size
});
this.render();
}
},
/** Pause upload process */
_eventStop: function() {
if (this.counter.running > 0) {
this.ui_button.model.set("status", "info");
$(".upload-top-info").html("Queue will pause after completing the current file...");
this.uploadbox.stop();
}
},
/** Remove all */
_eventReset: function() {
if (this.counter.running === 0) {
this.collection.reset();
this.counter.reset();
this.uploadbox.reset();
this.select_extension.value(this.options.default_extension);
this.select_genome.value(this.options.default_genome);
this.ui_button.model.set("percentage", 0);
this.render();
}
},
/** Update extension for all models */
_changeExtension: function(extension, defaults_only) {
var self = this;
this.collection.each(model => {
if (
model.get("status") == "init" &&
(model.get("extension") == self.options.default_extension || !defaults_only)
) {
model.set("extension", extension);
}
});
},
/** Update genome for all models */
_changeGenome: function(genome, defaults_only) {
var self = this;
this.collection.each(model => {
if (
model.get("status") == "init" &&
(model.get("genome") == self.options.default_genome || !defaults_only)
) {
model.set("genome", genome);
}
});
},
/** Package and upload ftp files in a single request */
_uploadFtp: function() {
var self = this;
var list = [];
this.collection.each(model => {
if (model.get("status") == "queued" && model.get("file_mode") == "ftp") {
self.uploadbox.remove(model.id);
list.push(model);
}
});
if (list.length > 0) {
$.uploadpost({
data: this.app.toData(list),
url: this.app.options.upload_path,
success: function(message) {
_.each(list, model => {
self._eventSuccess(model.id);
});
},
error: function(message) {
_.each(list, model => {
self._eventError(model.id, message);
});
}
});
}
},
/** Calculate percentage of all queued uploads */
_uploadPercentage: function(percentage, size) {
return (this.upload_completed + percentage * size) / this.upload_size;
},
/** Template */
_template: function() {
return `<div class="upload-view-default">
<div class="upload-top">
<div class="upload-top-info"/>
</div>
<div class="upload-box">
<div class="upload-helper">
<i class="fa fa-files-o"/>Drop files here
</div>
<table class="upload-table ui-table-striped" style="display: none;">
<thead>
<tr>
<th>Name</th>
<th>Size</th>
<th>Type</th>
<th>Genome</th>
<th>Settings</th>
<th>Status</th>
<th/>
</tr>
</thead>
<tbody/>
</table>
</div>
<div class="upload-footer">
<span class="upload-footer-title">Type (set all):</span>
<span class="upload-footer-extension"/>
<span class="upload-footer-extension-info upload-icon-button fa fa-search"/>
<span class="upload-footer-title">Genome (set all):</span>
<span class="upload-footer-genome"/>
</div>
<div class="upload-buttons"/>
</div>`;
}
});
@@ -1,58 +0,0 @@
/** View for upload/progress bar button */
import $ from "jquery";
import Backbone from "backbone";
import _l from "utils/localization";
var View = Backbone.View.extend({
initialize: function(options) {
var self = this;
this.model =
(options && options.model) ||
new Backbone.Model({
tooltip: _l("Download from URL or upload files from disk"),
label: "Load Data",
percentage: 0,
status: "",
onunload: function() {},
onclick: function() {}
}).set(options);
this.setElement(this._template());
this.$progress = this.$(".progress-bar");
this.listenTo(this.model, "change", this.render, this);
this.render();
$(window).on("beforeunload", () => self.model.get("onunload")());
},
render: function() {
var options = this.model.attributes;
this.$el
.off("click")
.on("click", e => {
options.onclick(e);
})
.tooltip({
title: this.model.get("tooltip"),
placement: "bottom",
trigger: "hover"
});
this.$progress
.removeClass()
.addClass("progress-bar")
.addClass("progress-bar-notransition")
.addClass(options.status != "" && `progress-bar-${options.status}`)
.css({ width: `${options.percentage}%` });
},
/** Template */
_template: function() {
return `<div class="upload-button">
<div class="progress">
<div class="progress-bar"/>
<a class="upload-button-link" href="javascript:void(0)" id="tool-panel-upload-button">
<span class="fa fa-upload"/>
</a>
</div>
</div>`;
}
});
export default { View: View };
@@ -1,6 +1,6 @@
import $ from "jquery";
import { getAppRoot } from "onload/loadConfig";
import Utils from "utils/utils";
import axios from "axios";
const AUTO_EXTENSION = {
id: "auto",
@@ -12,9 +12,11 @@ const DEFAULT_GENOME = "?";
const DEFAULT_EXTENSION = "auto";
function getUploadDatatypes(callback, datatypesDisableAuto, auto) {
Utils.get({
url: `${getAppRoot()}api/datatypes?extension_only=False`,
success: function(datatypes) {
const url = `${getAppRoot()}api/datatypes?extension_only=False`;
axios
.get(url)
.then(response => {
const datatypes = response.data;
const listExtensions = [];
for (var key in datatypes) {
listExtensions.push({
@@ -34,14 +36,18 @@ function getUploadDatatypes(callback, datatypesDisableAuto, auto) {
listExtensions.unshift(auto);
}
callback(listExtensions);
}
});
})
.catch(errorMessage => {
console.log(errorMessage);
});
}
function getUploadGenomes(callback, defaultGenome) {
Utils.get({
url: `${getAppRoot()}api/genomes`,
success: function(genomes) {
const url = `${getAppRoot()}api/genomes`;
axios
.get(url)
.then(response => {
const genomes = response.data;
const listGenomes = [];
for (var key in genomes) {
@@ -60,8 +66,10 @@ function getUploadGenomes(callback, defaultGenome) {
return a.text > b.text ? 1 : a.text < b.text ? -1 : 0;
});
callback(listGenomes);
}
});
})
.catch(errorMessage => {
console.log(errorMessage);
});
}
function getRemoteFiles(success, error) {
@@ -1,189 +0,0 @@
/** Upload app contains the upload progress button and upload modal, compiles model data for API request **/
import _l from "utils/localization";
import Backbone from "backbone";
import { getGalaxyInstance } from "app";
import Utils from "utils/utils";
import Modal from "mvc/ui/ui-modal";
import Tabs from "mvc/ui/ui-tabs";
import UploadUtils from "mvc/upload/upload-utils";
import UploadButton from "mvc/upload/upload-button";
import UploadViewDefault from "mvc/upload/default/default-view";
import UploadViewComposite from "mvc/upload/composite/composite-view";
import UploadViewCollection from "mvc/upload/collection/collection-view";
import UploadViewRuleBased from "mvc/upload/collection/rules-input-view";
export default Backbone.View.extend({
options: {
ftp_upload_site: "n/a",
default_genome: UploadUtils.DEFAULT_GENOME,
default_extension: UploadUtils.DEFAULT_EXTENSION,
height: 500,
width: 900,
auto: UploadUtils.AUTO_EXTENSION
},
// contains all available dataset extensions/types
list_extensions: [],
// contains all available genomes
list_genomes: [],
initialize: function(options) {
this.options = Utils.merge(options, this.options);
// create view for upload/progress button
this.ui_button = new UploadButton.View({
onclick: e => {
e.preventDefault();
this.show();
},
onunload: () => {
var percentage = this.ui_button.model.get("percentage", 0);
if (percentage > 0 && percentage < 100) {
return "Several uploads are queued.";
}
}
});
// set element to button view
this.setElement(this.ui_button.$el);
// load extensions
UploadUtils.getUploadDatatypes(
list_extensions => {
this.list_extensions = list_extensions;
},
this.options.datatypes_disable_auto,
this.options.auto
);
// load genomes
UploadUtils.getUploadGenomes(list_genomes => {
this.list_genomes = list_genomes;
}, this.default_genome);
},
/** Show/hide upload dialog */
show: function() {
const Galaxy = getGalaxyInstance();
var self = this;
if (!Galaxy.currHistoryPanel || !Galaxy.currHistoryPanel.model) {
window.setTimeout(() => {
self.show();
}, 500);
return;
}
this.current_user = Galaxy.user.id;
if (!this.modal) {
this.tabs = new Tabs.View();
this.default_view = new UploadViewDefault(this);
this.tabs.add({
id: "regular",
title: _l("Regular"),
$el: this.default_view.$el
});
this.composite_view = new UploadViewComposite(this);
this.tabs.add({
id: "composite",
title: _l("Composite"),
$el: this.composite_view.$el
});
this.collection_view = new UploadViewCollection(this);
this.tabs.add({
id: "collection",
title: _l("Collection"),
$el: this.collection_view.$el
});
this.rule_based_view = new UploadViewRuleBased(this);
this.tabs.add({
id: "rule-based",
title: _l("Rule-based"),
$el: this.rule_based_view.$el
});
this.modal = new Modal.View({
title: _l("Download from web or upload from disk"),
body: this.tabs.$el,
height: this.options.height,
width: this.options.width,
closing_events: true,
title_separator: false
});
}
this.modal.show();
},
/** Refresh user and current history */
currentHistory: function() {
const Galaxy = getGalaxyInstance();
return this.current_user && Galaxy.currHistoryPanel.model.get("id");
},
/** Get ftp configuration */
currentFtp: function() {
return this.current_user && this.options.ftp_upload_site;
},
/**
* Package API data from array of models
* @param{Array} items - Upload items/rows filtered from a collection
*/
toData: function(items, history_id) {
// create dictionary for data submission
var data = {
payload: {
tool_id: "upload1",
history_id: history_id || this.currentHistory(),
inputs: {}
},
files: [],
error_message: null
};
// add upload tools input data
if (items && items.length > 0) {
var inputs = {
file_count: items.length,
dbkey: items[0].get("genome", "?"),
// sometimes extension set to "" in automated testing after first upload of
// a session. https://github.com/galaxyproject/galaxy/issues/5169
file_type: items[0].get("extension") || "auto"
};
for (var index in items) {
var it = items[index];
it.set("status", "running");
if (it.get("file_size") > 0) {
var prefix = `files_${index}|`;
inputs[`${prefix}type`] = "upload_dataset";
if (it.get("file_name") != "New File") {
inputs[`${prefix}NAME`] = it.get("file_name");
}
inputs[`${prefix}space_to_tab`] = (it.get("space_to_tab") && "Yes") || null;
inputs[`${prefix}to_posix_lines`] = (it.get("to_posix_lines") && "Yes") || null;
inputs[`${prefix}dbkey`] = it.get("genome", null);
inputs[`${prefix}file_type`] = it.get("extension", null);
switch (it.get("file_mode")) {
case "new":
inputs[`${prefix}url_paste`] = it.get("url_paste");
break;
case "ftp":
inputs[`${prefix}ftp_files`] = it.get("file_path");
break;
case "local":
data.files.push({
name: `${prefix}file_data`,
file: it.get("file_data")
});
}
} else if (it.get("optional")) {
continue;
} else {
data.error_message = "Upload content incomplete.";
it.set("status", "error");
it.set("info", data.error_message);
break;
}
}
data.payload.inputs = JSON.stringify(inputs);
}
return data;
}
});
@@ -0,0 +1,84 @@
/**
* shared functionality between default-row and collection-row.
*/
import _l from "utils/localization";
import Backbone from "backbone";
import Utils from "utils/utils";
import Popover from "mvc/ui/ui-popover";
import UploadSettings from "mvc/upload/upload-settings";
export default Backbone.View.extend({
/** Dictionary of upload states and associated icons */
status_classes: {
init: "upload-icon-button fa fa-trash-o",
queued: "upload-icon fa fa-spinner fa-spin",
running: "upload-icon fa fa-spinner fa-spin",
success: "upload-icon-button fa fa-check",
error: "upload-icon-button fa fa-exclamation-triangle"
},
_setupSettings: function() {
// append popup to settings icon
this.settings = new Popover({
title: _l("Upload configuration"),
container: this.$(".upload-settings"),
placement: "bottom"
});
},
_renderStatusType: function(status) {
if (status == "success") {
this.$el.addClass("table-success");
this.$percentage.html("100%");
} else if (status == "error") {
this.$el.addClass("table-danger");
this.$info_progress.hide();
} else if (status == "warning") {
this.$el.addClass("table-warning");
this.$info_progress.hide();
}
},
_setupUploadBoxListeners: function() {
this.listenTo(this.model, "change:percentage", () => {
this._refreshPercentage();
});
this.listenTo(this.model, "change:status", () => {
this._refreshStatus();
});
this.listenTo(this.model, "change:info", () => {
this._refreshInfo();
});
this.listenTo(this.model, "change:file_size", () => {
this._refreshFileSize();
});
},
_refreshInfo: function() {
var info = this.model.get("info");
if (info) {
this.$info_text.html(`<strong>Warning: </strong>${info}`).show();
} else {
this.$info_text.hide();
}
},
_refreshPercentage: function() {
var percentage = parseInt(this.model.get("percentage"));
this.$progress_bar.css({ width: `${percentage}%` });
this.$percentage.html(percentage != 100 ? `${percentage}%` : "Adding to history...");
},
_refreshFileSize: function() {
this.$size.html(Utils.bytesToString(this.model.get("file_size")));
},
_removeRow: function() {
if (["init", "success", "error"].indexOf(this.model.get("status")) !== -1) {
this.app.collection.remove(this.model);
}
},
_showSettings: function() {
this.settings.show(new UploadSettings(this).$el);
}
});
@@ -14,9 +14,16 @@ var requestCharts = function(chart, module) {
var group_index = 0;
for (var key in chart.settings.attributes) {
var settings_value = chart.settings.get(key);
_.each([[" ", "&#32;"], [",", "&#44;"], [":", "&#58;"]], function(pair) {
settings_value = settings_value.replace(new RegExp(pair[0], "g"), pair[1]);
});
_.each(
[
[" ", "&#32;"],
[",", "&#44;"],
[":", "&#58;"]
],
function(pair) {
settings_value = settings_value.replace(new RegExp(pair[0], "g"), pair[1]);
}
);
settings_string += key + ":" + settings_value + ", ";
}
settings_string = settings_string.substring(0, settings_string.length - 2);
+1 -4
View File
@@ -44,10 +44,7 @@ const currentConfig$ = new BehaviorSubject(defaultConfigs);
mergedConfigs.subscribe(currentConfig$);
// debounce, filter out initial null and other invalid stuff
export const config$ = currentConfig$.asObservable().pipe(
filter(Boolean),
debounceTime(100)
);
export const config$ = currentConfig$.asObservable().pipe(filter(Boolean), debounceTime(100));
/**
* Adds config objects into the global configuration.

Some files were not shown because too many files have changed in this diff Show More