Merge branch 'dev' into sybila-tools-datatypes

This commit is contained in:
Matej Troják
2022-08-31 12:07:18 +01:00
committed by GitHub
667 changed files with 17942 additions and 14674 deletions
+1 -1
View File
@@ -93,7 +93,7 @@ jobs:
- run: tox -e validate_test_tools
js_lint:
docker:
- image: cimg/node:16.13.2
- image: cimg/node:16.15.1
<<: *set_workdir
steps:
- *restore_yarn_cache
+3 -3
View File
@@ -22,9 +22,9 @@ jobs:
id: branch
run: echo "::set-output name=name::$(BRANCH_NAME=${GITHUB_REF##*/}; echo ${BRANCH_NAME/release_/})"
- name: Build container image
run: docker build . --build-arg GIT_COMMIT=$(git rev-parse HEAD) --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') --build-arg IMAGE_TAG=${{ steps.branch.outputs.name }} -t galaxyproject/galaxy-min:${{ steps.branch.outputs.name }} -t quay.io/galaxyproject/galaxy-min:${{ steps.branch.outputs.name }} -f .k8s_ci.Dockerfile
run: docker build . --build-arg GIT_COMMIT=$(git rev-parse HEAD) --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') --build-arg IMAGE_TAG=${{ steps.branch.outputs.name }} -t galaxy/galaxy-min:${{ steps.branch.outputs.name }} -t quay.io/galaxyproject/galaxy-min:${{ steps.branch.outputs.name }} -f .k8s_ci.Dockerfile
- name: Create auto-expiring one for per-commit auto repository
run: echo "FROM galaxyproject/galaxy-min:${{ steps.branch.outputs.name }}" | docker build --label "quay.expires-after"="90d" -t "quay.io/galaxyproject/galaxy-k8s-auto:${{ steps.commit.outputs.sha_short }}" -
run: echo "FROM galaxy/galaxy-min:${{ steps.branch.outputs.name }}" | docker build --label "quay.expires-after"="90d" -t "quay.io/galaxyproject/galaxy-k8s-auto:${{ steps.commit.outputs.sha_short }}" -
- name: Login to quay.io
uses: actions-hub/docker/login@master
env:
@@ -47,4 +47,4 @@ jobs:
- name: Push to DockerHub with branch name
uses: actions-hub/docker@master
with:
args: push galaxyproject/galaxy-min:${{ steps.branch.outputs.name }}
args: push galaxy/galaxy-min:${{ steps.branch.outputs.name }}
+4 -4
View File
@@ -18,11 +18,11 @@
ARG ROOT_DIR=/galaxy
ARG SERVER_DIR=$ROOT_DIR/server
ARG STAGE1_BASE=python:3.7-slim
ARG STAGE1_BASE=python:3.10-slim
ARG FINAL_STAGE_BASE=$STAGE1_BASE
ARG GALAXY_USER=galaxy
ARG GALAXY_PLAYBOOK_REPO=https://github.com/galaxyproject/galaxy-docker-k8s
ARG GALAXY_PLAYBOOK_BRANCH=v2.0.0
ARG GALAXY_PLAYBOOK_BRANCH=v3.0.0
ARG GIT_COMMIT=unspecified
ARG BUILD_DATE=unspecified
@@ -51,7 +51,7 @@ RUN set -xe; \
libc-dev \
bzip2 \
gcc \
&& pip install --no-cache virtualenv 'ansible<2.10' \
&& pip install --no-cache virtualenv ansible \
&& apt-get autoremove -y && apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/*
@@ -72,7 +72,7 @@ COPY . $SERVER_DIR/
FROM stage1 AS server_build
ARG SERVER_DIR
RUN ansible-playbook -i localhost, playbook.yml -v -e galaxy_build_client=False -e galaxy_virtualenv_command=virtualenv
RUN ansible-playbook -i localhost, playbook.yml -v -e "{galaxy_build_client: false}" -e galaxy_virtualenv_command=virtualenv
RUN cat /galaxy/server/lib/galaxy/dependencies/conditional-requirements.txt | grep psycopg2-binary | xargs /galaxy/server/.venv/bin/pip install
+4 -115
View File
@@ -1,118 +1,7 @@
# Galaxy Project Code of Conduct
## Note: Change is Coming
This project is committed to providing a welcoming and harassment-free
experience for everyone. We therefore expect participants to abide by our Code
of Conduct, which can be found at:
Since 2015 the Galaxy Project Code of Conduct has resided here, and updates to it have been coordinated by the Committers Group for this repo.
This code of conduct has served us reasonably well over the years, but it is time for a refresh. Since 2015 the world has become much more aware of the need for codes of conduct, and best practices for creating and enforcing them have moved forward significantly.
**As a result it is past time to update both our code of conduct and our process for maintaining it.** The Galaxy Outreach and Training [Working Group](https://galaxyproject.org/community/wg/) is currently working on a first draft of a new code and new processes. The final code and processes we adopt will be [implemented](https://galaxyproject.org/community/coc/) only after it has been discussed, revised, and approved by the community.
Until that happens this will remain the Galaxy Project Code of Conduct.
# Code of Conduct
This code of conduct outlines our expectations for participants within the
Galaxy community, as well as steps to reporting unacceptable behavior. We are
committed to providing a welcoming and inspiring community for all and expect
our code of conduct to be honored. Anyone who violates this code of conduct may
be banned from the community.
Our open source community strives to:
* **Be friendly and patient.**
* **Be welcoming**: We strive to be a community that welcomes and
supports people of all backgrounds and identities. This includes, but is not
limited to members of any race, ethnicity, culture, national origin, colour,
immigration status, social and economic class, educational level, sex, sexual
orientation, gender identity and expression, age, size, family status,
political belief, religion, and mental and physical ability.
* **Be considerate**: Your work will be used by other people, and you in turn
will depend on the work of others. Any decision you take will affect users
and colleagues, and you should take those consequences into account when
making decisions. Remember that we're a world-wide community, so you might
not be communicating in someone else's primary language.
* **Be respectful**: Not all of us will agree all the time, but disagreement is
no excuse for poor behavior and poor manners. We might all experience some
frustration now and then, but we cannot allow that frustration to turn into a
personal attack. It’s important to remember that a community where people
feel uncomfortable or threatened is not a productive one.
* **Be careful in the words that we choose**: We are a community of
professionals, and we conduct ourselves professionally. Be kind to others. Do
not insult or put down other participants. Harassment and other exclusionary
behavior aren't acceptable. This includes, but is not limited to: Violent
threats or language directed against another person, Discriminatory jokes and
language, Posting sexually explicit or violent material, Posting (or
threatening to post) other people’s personally identifying information
(“doxing”), Personal insults, especially those using racist or sexist terms,
Unwelcome sexual attention, Advocating for, or encouraging, any of the above
behavior, Repeated harassment of others. In general, if someone asks you to
stop, then stop.
* **Try to understand why we disagree**: Disagreements, both social and
technical, happen all the time. It is important that we resolve disagreements
and differing views constructively. Remember that we’re different. Diversity
contributes to the strength of our community, which is composed of people
from a wide range of backgrounds. Different people have different
perspectives on issues. Being unable to understand why someone holds a
viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to
err and blaming each other doesn’t get us anywhere. Instead, focus on helping
to resolve issues and learning from mistakes.
### Diversity Statement
We encourage everyone to participate and are committed to building a community
for all. Although we will fail at times, we seek to treat everyone both as
fairly and equally as possible. Whenever a participant has made a mistake, we
expect them to take responsibility for it. If someone has been harmed or
offended, it is our responsibility to listen carefully and respectfully, and do
our best to right the wrong.
Although this list cannot be exhaustive, we explicitly honor diversity in age,
gender, gender identity or expression, culture, ethnicity, language, national
origin, political beliefs, profession, race, religion, sexual orientation,
socioeconomic status, and technical ability. We will not tolerate
discrimination based on any of the protected characteristics above, including
participants with disabilities.
### Reporting Issues
If you experience or witness unacceptable behavior, or have any other concerns,
please report it to any combination of the following people that makes you feel
the most comfortable:
- Dave Clements (clementsgalaxy@gmail.com). Dave is the Galaxy Project community
outreach manager and has experience handling Code of Conduct related issues.
- Dr. Mike Schatz (mschatz@cs.jhu.edu). Mike is Dave Clements' direct manager
and issues related to Dave in some way should be reported to Mike.
- Helena Rasche (helena.rasche@gmail.com). Helena is a well-known, trusted
community member, is LGBT+, and has completely separate funding and
institutional affiliation from Dave and Mike.
All reports will be handled with discretion. In your report please include:
- Your contact information.
- Names (real, nicknames, or pseudonyms) of any individuals involved. If there
are additional witnesses, please include them as well. Your account of what
occurred, and if you believe the incident is ongoing. If there is a publicly
available record (e.g. a mailing list archive or a public IRC logger), please
include a link.
- Any additional information that may be helpful.
After filing a report, a representative will contact you personally, review the
incident, follow up with any additional questions, and make a decision as to
how to respond. If the person who is harassing you is part of the response
team, they will recuse themselves from handling your incident. If the complaint
originates from a member of the response team, it will be handled by a
different member of the response team. We will respect confidentiality requests
for the purpose of protecting victims of abuse.
### Attribution & Acknowledgements
This code of conduct is based on the Open Code of Conduct from the TODOGroup.
https://galaxyproject.org/community/coc/
+1 -1
View File
@@ -158,7 +158,7 @@ https://help.github.com/en/github/getting-started-with-github/git-and-github-lea
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
`.flake8` file
- The Python code base is automatically formatted using
[isort](https://pycqa.github.io/isort/) (for imports) and
[black](https://black.readthedocs.io). To easily format your Python code
+2
View File
@@ -8,6 +8,7 @@ The following individuals have contributed code to Galaxy:
* Guruprasad Ananda <gua110@bx.psu.edu>
* Evgeny Anatskiy <evgeny.anatskiy@gmail.com>
* Florent Angly <florent.angly@gmail.com>
* Patrick Austin <patrick.austin@stfc.ac.uk>
* Raj Ayyampalayam <raj76@uga.edu>
* Abdulrahman Azab <eng.azab@gmail.com>
* Finn Bacall <finn.bacall@cs.man.ac.uk>
@@ -34,6 +35,7 @@ The following individuals have contributed code to Galaxy:
* Richard Burhans <burhans@bx.psu.edu>
* Jennifer Cabral <jencabral@gmail.com>
* Martin Čech <marten@bx.psu.edu>
* Eli Chadwick <eli.chadwick@stfc.ac.uk>
* Ramkrishna Chakrabarty <rc@bx.psu.edu>
* Matt Chambers <matt.chambers42@gmail.com>
* Brad Chapman <chapmanb@50mail.com>
+6 -1
View File
@@ -4,7 +4,6 @@ VENV?=.venv
IN_VENV=if [ -f "$(VENV)/bin/activate" ]; then . "$(VENV)/bin/activate"; fi;
RELEASE_CURR:=22.09
RELEASE_UPSTREAM:=upstream
TARGET_BRANCH=$(RELEASE_UPSTREAM)/dev
CONFIG_MANAGE=$(IN_VENV) python lib/galaxy/config/config_manage.py
PROJECT_URL?=https://github.com/galaxyproject/galaxy
DOCS_DIR=doc
@@ -45,6 +44,9 @@ format: ## Format Python code base
$(IN_VENV) isort .
$(IN_VENV) black .
remove-unused-imports: ## Remove unused imports in Python code base
$(IN_VENV) autoflake --in-place --remove-all-unused-imports --recursive --verbose lib/ test/
list-dependency-updates: setup-venv
$(IN_VENV) pip list --outdated --format=columns
@@ -159,6 +161,9 @@ update-cwl-conformance-tests: ## update CWL conformance tests
$(MAKE) clean-cwl-conformance-tests
$(MAKE) generate-cwl-conformance-tests
skip-client: ## Run only the server, skipping the client build.
GALAXY_SKIP_CLIENT_BUILD=1 sh run.sh
node-deps: ## Install NodeJS dependencies.
ifndef YARN
@echo "Could not find yarn, which is required to build the Galaxy client.\nTo install yarn, please visit \033[0;34mhttps://yarnpkg.com/en/docs/install\033[0m for instructions, and package information for all platforms.\n"
+1 -1
View File
@@ -1 +1 @@
16.13.2
16.15.1
+8
View File
@@ -86,6 +86,14 @@ Sometimes you want to run your local UI against a remote Galaxy server. This is
CHANGE_ORIGIN=true GALAXY_URL="https://usegalaxy.org/" make client-dev-server
## Running a Separate Server
When developing the client it can be helpful to run a local server for the
client to connect to, and run the client separately with one of the above
commands. This command will run galaxy without building the client:
make skip-client
## Changing Styles/CSS
Galaxy uses Sass for its styling, which is a superset of CSS that compiles down
+39 -36
View File
@@ -17,7 +17,9 @@
"not baidu 7.12"
],
"resolutions": {
"**/chokidar": "3.5.3"
"chokidar": "3.5.3",
"vue": "2.7.8",
"vue-loader": "15.10.0"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.4",
@@ -34,11 +36,12 @@
"assert": "^2.0.0",
"axios": "^0.25.0",
"babel-runtime": "^6.26.0",
"backbone": "1.4.0",
"backbone": "1.4.1",
"bootstrap": "4.6",
"bootstrap-vue": "^2.21.2",
"citation-js": "^0.5.5",
"citation-js": "^0.6.4",
"core-js": "^3.21.0",
"csv-parse": "^5.3.0",
"d3": "3",
"date-fns": "^2.28.0",
"date-fns-tz": "^1.3.3",
@@ -54,16 +57,16 @@
"jquery": "2",
"jquery-migrate": "~1.4",
"jquery-mousewheel": "^3.1.13",
"jquery-ui": "^1.13.1",
"jquery-ui": "^1.13.2",
"jspdf": "^2.5.1",
"linkify-html": "^3.0.5",
"linkifyjs": "^3.0.5",
"localforage": "^1.10.0",
"markdown-it": "^12.3.2",
"markdown-it-regexp": "^0.4.0",
"object-hash": "^2.2.0",
"object-hash": "^3.0.0",
"popper.js": "^1.16.1",
"pretty-bytes": "^5.6.0",
"pretty-bytes": "^6.0.0",
"pyre-to-regexp": "^0.0.5",
"querystring-es3": "^0.2.1",
"regenerator-runtime": "^0.13.9",
@@ -80,7 +83,7 @@
"tus-js-client": "^2.3.0",
"underscore": "^1.13.2",
"util": "^0.12.4",
"vue": "^2.6.14",
"vue": "^2.7.8",
"vue-infinite-scroll": "^2.0.2",
"vue-multiselect": "^2.1.6",
"vue-observe-visibility": "^1.0.0",
@@ -93,7 +96,7 @@
"vuex": "^3.6.2",
"vuex-cache": "^3.4.0",
"vuex-persist": "2.3",
"winbox": "^0.2.1",
"winbox": "^0.2.4",
"xml-beautifier": "^0.5.0"
},
"scripts": {
@@ -120,35 +123,35 @@
"eslint": "eslint -c .eslintrc.json --ext .js,.vue src"
},
"devDependencies": {
"@babel/core": "^7.17.0",
"@babel/eslint-parser": "^7.17.0",
"@babel/helper-validator-identifier": "^7.16.7",
"@babel/core": "^7.18.9",
"@babel/eslint-parser": "^7.18.9",
"@babel/helper-validator-identifier": "^7.18.6",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.17.0",
"@babel/preset-env": "^7.16.11",
"@cerner/duplicate-package-checker-webpack-plugin": "^2.1.0",
"@testing-library/jest-dom": "^5.16.2",
"@babel/plugin-transform-runtime": "^7.18.9",
"@babel/preset-env": "^7.18.9",
"@cerner/duplicate-package-checker-webpack-plugin": "^2.3.0",
"@testing-library/jest-dom": "^5.16.4",
"@vue/test-utils": "^1.3.0",
"@vue/vue2-jest": "^27.0.0-alpha.4",
"@vue/vue2-jest": "^28.0.1",
"amdi18n-loader": "^0.9.3",
"autoprefixer": "^10.4.2",
"autoprefixer": "10.4.5",
"axios-mock-adapter": "^1.20.0",
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "^27.4.6",
"babel-loader": "^8.2.3",
"babel-plugin-transform-inline-environment-variables": "^0.4.3",
"babel-jest": "^28.1.3",
"babel-loader": "^8.2.5",
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
"babel-plugin-transform-vue-template": "^0.4.2",
"buffer": "^6.0.3",
"css-loader": "^6.6.0",
"css-minimizer-webpack-plugin": "^3.4.1",
"css-loader": "^6.7.1",
"css-minimizer-webpack-plugin": "^4.0.0",
"del": "^6.0.0",
"eslint": "^8.13.0",
"eslint": "^8.20.0",
"eslint-plugin-compat": "^4.0.2",
"eslint-plugin-vue": "^8.6.0",
"expose-loader": "^3.1.0",
"eslint-plugin-vue": "^9.3.0",
"expose-loader": "^4.0.0",
"gulp": "^4.0.2",
"ignore-loader": "^0.1.2",
"imports-loader": "^3.1.1",
"imports-loader": "^4.0.0",
"jest": "^27.4.7",
"jest-location-mock": "^1.0.9",
"jest-raw-loader": "^1.0.1",
@@ -160,23 +163,23 @@
"karma-qunit": "^4.1.2",
"karma-webpack": "^5.0.0",
"mini-css-extract-plugin": "^2.5.3",
"postcss-loader": "^6.2.1",
"prettier": "^2.5.1",
"postcss-loader": "^7.0.1",
"prettier": "^2.7.1",
"process": "^0.11.10",
"qunit": "^2.17.2",
"raw-loader": "^4.0.2",
"sass": "^1.49.7",
"sass-loader": "^12.4.0",
"sass": "^1.53.0",
"sass-loader": "^13.0.2",
"sinon": "^13.0.1",
"store": "^2.0.12",
"style-loader": "^3.3.1",
"vue-loader": "^15.9.8",
"vue-template-compiler": "^2.6.14",
"webpack": "^5.68.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.4",
"vue-loader": "^15.10.0",
"vue-template-compiler": "^2.7.8",
"webpack": "^5.73.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.3",
"webpack-merge": "^5.8.0",
"yaml-loader": "^0.6.0"
"yaml-loader": "^0.8.0"
},
"peerDependencies": {
"postcss": "^8.4.6"
-2
View File
@@ -62,7 +62,6 @@ export function multiHistory(options) {
}
// Previously wandering around as window.thing = thing in the onload script
export { default as panels } from "layout/panel";
export { show_in_overlay, hide_modal, show_message, show_modal, Modal } from "layout/modal";
export { make_popupmenu, make_popup_menus } from "ui/popupmenu";
export { render_embedded_items } from "mvc/embedded-objects";
@@ -75,7 +74,6 @@ export function chart(options) {
}
export { initMasthead } from "components/Masthead/initMasthead";
export { panelManagement } from "onload/globalInits/panelManagement";
export { mountMakoTags } from "components/Tags";
export { mountWorkflowEditor } from "components/Workflow/Editor/mount";
export { mountPageEditor } from "components/PageEditor/mount";
+72
View File
@@ -0,0 +1,72 @@
<script setup>
/* global __buildTimestamp__, __license__ */
/* (injected by webpack) */
import { computed } from "vue";
import { getAppRoot } from "onload/loadConfig";
import { useConfig } from "composables/useConfig";
import UtcDate from "components/UtcDate";
import License from "components/License/License";
import ExternalLink from "components/ExternalLink";
const { config, isLoaded } = useConfig();
const clientBuildDate = __buildTimestamp__ || new Date();
const apiDocsLink = `${getAppRoot()}api/docs`;
const galaxyLicense = __license__;
const versionUserDocumentationUrl = computed(() => {
const configVal = config.value;
return config.value.version_minor.slice(0, 3) === "dev"
? "https://docs.galaxyproject.org/en/latest/releases/index.html"
: `${configVal.release_doc_base_url}${configVal.version_major}/releases/${configVal.version_major}_announce_user.html`;
});
</script>
<template>
<div v-if="isLoaded">
<h1>About This Galaxy</h1>
<div>
<!-- Galaxy version (detailed), with a link to the release notes -->
<h2>Galaxy Version Information</h2>
<p>
The Galaxy Server is running version
<external-link :href="versionUserDocumentationUrl">
<strong> {{ config.version_major }}.{{ config.version_minor }}</strong> </external-link
>, and the web client was built on <UtcDate :date="clientBuildDate" mode="pretty" />.
</p>
<template v-if="config.version_extra">
<p>The server also provides the following extra version information</p>
<ul>
<li v-for="(value, name, index) in config.version_extra" :key="index">
<strong>{{ name }}</strong>
: {{ value }}
</li>
</ul>
</template>
</div>
<div>
<h2>Galaxy API Documentation</h2>
<!-- API documentation link -->
<p>
The Galaxy API is available, and explorable, at
<external-link :href="apiDocsLink">
{{ apiDocsLink }}
</external-link>
</p>
</div>
<div>
<h2>License Information</h2>
<p>The Galaxy Software is licensed under <License :license-id="galaxyLicense" /></p>
</div>
<div v-if="config.terms_url">
<!-- Terms, if available.-->
<h2>Terms and Conditions</h2>
<p>
This Galaxy Server has specified Terms and Conditions that apply to use of the service.
<external-link :href="config.terms_url">Review them here.</external-link>
</p>
</div>
</div>
</template>
+8 -10
View File
@@ -28,6 +28,7 @@
</div>
</template>
<script>
import store from "store";
import { getAppRoot } from "onload/loadConfig";
import { getGalaxyInstance } from "app";
import { Services } from "./services";
@@ -147,16 +148,13 @@ export default {
this.onError(error);
});
},
onShowDataset(item) {
const Galaxy = getGalaxyInstance();
this.services
.setHistory(item.history_id)
.then(() => {
Galaxy.currHistoryPanel.loadCurrentHistory();
})
.catch((error) => {
this.onError(error);
});
async onShowDataset(item) {
const historyId = item.history_id;
try {
await store.dispatch("history/setCurrentHistory", historyId);
} catch (error) {
this.onError(error);
}
},
onTags(tags, index) {
const item = this.rows[index];
-10
View File
@@ -45,16 +45,6 @@ export class Services {
}
}
async setHistory(id) {
const url = `${this.root}history/set_as_current?id=${id}`;
try {
const response = await axios.get(url);
return response.data;
} catch (e) {
this._errorMessage(e);
}
}
async updateTags(item_id, item_class, item_tags) {
const url = `${this.root}api/tags`;
try {
@@ -42,7 +42,8 @@ describe("DatasetAttributes", () => {
expect(wrapper.findAll("#conversion_text").length).toBe(1);
expect(wrapper.findAll("#datatype_text").length).toBe(1);
expect(wrapper.findAll("#permission_text").length).toBe(1);
expect(wrapper.findAll(".tab-pane").length).toBe(4);
expect(wrapper.findAll(".tab-pane").length).toBe(3);
expect(wrapper.findAll(".ui-portlet-section").length).toBe(2);
const $button = wrapper.find("#dataset-attributes-default-save");
await $button.trigger("click");
await flushPromises();
@@ -57,5 +58,6 @@ describe("DatasetAttributes", () => {
expect(wrapper.findAll("#datatype_text").length).toBe(1);
expect(wrapper.findAll("#permission_text").length).toBe(1);
expect(wrapper.findAll(".tab-pane").length).toBe(3);
expect(wrapper.findAll(".ui-portlet-section").length).toBe(1);
});
});
@@ -25,29 +25,53 @@
</b-button>
</div>
</b-tab>
<b-tab v-if="!result['conversion_disable']">
<b-tab v-if="!result['conversion_disable'] || !result['datatype_disable']">
<template v-slot:title>
<font-awesome-icon icon="cog" class="mr-1" />{{ "Convert" | l }}
<span v-if="!result['conversion_disable']">
<font-awesome-icon icon="cog" class="mr-1" />{{ "Convert" | l }}
</span>
<span v-else>
<font-awesome-icon icon="database" class="mr-1" />{{ "Datatypes" | l }}
</span>
</template>
<FormDisplay :inputs="result['conversion_inputs']" @onChange="onConversion" />
<div class="mt-2">
<b-button variant="primary" @click="submit('conversion', 'conversion')">
<font-awesome-icon icon="exchange-alt" class="mr-1" />{{ "Create Dataset" | l }}
</b-button>
<div v-if="!result['conversion_disable']" class="ui-portlet-section">
<div class="portlet-header">
<div class="portlet-title">
<font-awesome-icon icon="cog" class="portlet-title-icon fa-fw mr-1" />
<span class="portlet-title-text">
<b itemprop="name">{{ "Convert" | l }}</b>
</span>
</div>
</div>
<div class="portlet-content">
<FormDisplay :inputs="result['conversion_inputs']" @onChange="onConversion" />
<div class="mt-2">
<b-button variant="primary" @click="submit('conversion', 'conversion')">
<font-awesome-icon icon="exchange-alt" class="mr-1" />{{ "Create Dataset" | l }}
</b-button>
</div>
</div>
</div>
</b-tab>
<b-tab v-if="!result['datatype_disable']">
<template v-slot:title>
<font-awesome-icon icon="database" class="mr-1" />{{ "Datatypes" | l }}
</template>
<FormDisplay :inputs="result['datatype_inputs']" @onChange="onDatatype" />
<div class="mt-2">
<b-button variant="primary" class="mr-1" @click="submit('datatype', 'datatype')">
<font-awesome-icon icon="save" class="mr-1" />{{ "Save" | l }}
</b-button>
<b-button @click="submit('datatype', 'datatype_detect')">
<font-awesome-icon icon="redo" class="mr-1" />{{ "Auto-detect" | l }}
</b-button>
<div v-if="!result['datatype_disable']" class="ui-portlet-section">
<div class="portlet-header">
<div class="portlet-title">
<font-awesome-icon icon="database" class="portlet-title-icon fa-fw mr-1" />
<span class="portlet-title-text">
<b itemprop="name">{{ "Datatypes" | l }}</b>
</span>
</div>
</div>
<div class="portlet-content">
<FormDisplay :inputs="result['datatype_inputs']" @onChange="onDatatype" />
<div class="mt-2">
<b-button variant="primary" class="mr-1" @click="submit('datatype', 'datatype')">
<font-awesome-icon icon="save" class="mr-1" />{{ "Save" | l }}
</b-button>
<b-button @click="submit('datatype', 'datatype_detect')">
<font-awesome-icon icon="redo" class="mr-1" />{{ "Auto-detect" | l }}
</b-button>
</div>
</div>
</div>
</b-tab>
<b-tab v-if="!result['permission_disable']">
@@ -10,7 +10,7 @@
<JobDetailsProvider
v-if="!isDatasetLoading && dataset.creating_job !== null"
v-slot="{ result: job, loading: isJobLoading }"
:jobId="dataset.creating_job"
:job-id="dataset.creating_job"
auto-refresh>
<div v-if="!isJobLoading">
<dataset-information class="detail" :hda_id="datasetId" />
@@ -8,7 +8,7 @@
<JobDetailsProvider
v-if="!datasetLoading"
v-slot="{ result: jobDetails, loading }"
:jobId="dataset.creating_job"
:job-id="dataset.creating_job"
@error="onError">
<div v-if="!loading">
<div class="page-container edit-attr">
@@ -24,7 +24,10 @@
:tool-stderr="jobDetails.tool_stderr"
:job-stderr="jobDetails.job_stderr"
:job-messages="jobDetails.job_messages" />
<JobProblemProvider v-slot="{ result: jobProblems }" :jobId="dataset.creating_job" @error="onError">
<JobProblemProvider
v-slot="{ result: jobProblems }"
:job-id="dataset.creating_job"
@error="onError">
<div v-if="jobProblems && (jobProblems.has_duplicate_inputs || jobProblems.has_empty_inputs)">
<h3 class="common_problems mt-3">Detected Common Potential Problems</h3>
<p v-if="jobProblems.has_empty_inputs" id="dataset-error-has-empty-inputs">
+21
View File
@@ -0,0 +1,21 @@
<script setup>
import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { library } from "@fortawesome/fontawesome-svg-core";
library.add(faExternalLinkAlt);
const props = defineProps({
href: {
type: String,
required: true,
},
});
</script>
<template>
<a target="_blank" :href="props.href">
<slot></slot>
<font-awesome-icon icon="external-link-alt" />
</a>
</template>
+1 -1
View File
@@ -28,8 +28,8 @@
</div>
<FormCard
v-for="(cache, cacheId) in input.cache"
data-description="repeat block"
:key="cacheId"
data-description="repeat block"
:title="repeatTitle(cacheId, input.title)">
<template v-slot:operations>
<b-button
@@ -1,7 +1,7 @@
<template>
<div
:id="contentId"
:class="['content-item m-1 p-0 rounded content-buttons', contentCls]"
:class="['content-item m-1 p-0 rounded btn-transparent-background', contentCls]"
:data-hid="id"
:data-state="state">
<div class="p-1 cursor-pointer" draggable @dragstart="onDragStart" @click.stop="onClick">
@@ -57,6 +57,7 @@
:is-history-item="isHistoryItem"
:is-visible="item.visible"
:state="state"
:item-urls="itemUrls"
@delete="$emit('delete')"
@display="onDisplay"
@edit="onEdit"
@@ -85,6 +86,7 @@
v-if="expandDataset"
:dataset="item"
:show-highlight="isHistoryItem"
:item-urls="itemUrls"
@edit="onEdit"
@toggleHighlights="toggleHighlights" />
</b-collapse>
@@ -169,6 +171,26 @@ export default {
tagsDisabled() {
return !this.expandDataset || !this.isHistoryItem;
},
isCollection() {
return "collection_type" in this.item;
},
/** Relative URLs for history item actions */
itemUrls() {
const id = this.item.id;
if (this.isCollection) {
return {
edit: `/collection/edit/${id}`,
};
}
return {
display: `/datasets/${id}/display/?preview=True`,
edit: `/datasets/edit/${id}`,
showDetails: `/datasets/${id}/details`,
reportError: `/datasets/${id}/error`,
rerun: `/tool_runner/rerun?id=${id}`,
visualize: `/visualizations?dataset_id=${id}`,
};
},
},
methods: {
onClick() {
@@ -179,8 +201,7 @@ export default {
}
},
onDisplay() {
const url = `datasets/${this.item.id}/display/?preview=True`;
iframeAdd({ path: url, title: this.name });
iframeAdd({ path: this.itemUrls.display, title: this.name });
},
onDragStart(evt) {
evt.dataTransfer.dropEffect = "move";
@@ -188,11 +209,7 @@ export default {
evt.dataTransfer.setData("text", JSON.stringify([this.item]));
},
onEdit() {
if (this.item.collection_type) {
this.$router.push(`/collection/edit/${this.item.id}`);
} else {
this.$router.push(`/datasets/edit/${this.item.id}`);
}
this.$router.push(this.itemUrls.edit);
},
onTags(newTags) {
this.$emit("tag-change", this.item, newTags);
@@ -207,7 +224,7 @@ export default {
},
};
</script>
<style>
<style lang="scss">
.content-item:hover {
filter: brightness(105%);
}
@@ -4,25 +4,27 @@
v-if="isDataset"
:disabled="displayDisabled"
:title="displayButtonTitle"
class="px-1"
class="display-btn px-1"
size="sm"
variant="link"
@click.stop="$emit('display')">
:href="displayUrl"
@click.prevent.stop="$emit('display')">
<icon icon="eye" />
</b-button>
<b-button
v-if="isHistoryItem"
:disabled="editDisabled"
:title="editButtonTitle"
class="px-1"
class="edit-btn px-1"
size="sm"
variant="link"
@click.stop="$emit('edit')">
:href="editUrl"
@click.prevent.stop="$emit('edit')">
<icon icon="pen" />
</b-button>
<b-button
v-if="isHistoryItem && !isDeleted"
class="px-1"
class="delete-btn px-1"
title="Delete"
size="sm"
variant="link"
@@ -31,7 +33,7 @@
</b-button>
<b-button
v-if="isHistoryItem && isDeleted"
class="px-1"
class="undelete-btn px-1"
title="Undelete"
size="sm"
variant="link"
@@ -40,7 +42,7 @@
</b-button>
<b-button
v-if="isHistoryItem && !isVisible"
class="px-1"
class="unhide-btn px-1"
title="Unhide"
size="sm"
variant="link"
@@ -51,6 +53,7 @@
</template>
<script>
import { prependPath } from "utils/redirect.js";
export default {
props: {
isDataset: { type: Boolean, required: true },
@@ -58,6 +61,7 @@ export default {
isHistoryItem: { type: Boolean, required: true },
isVisible: { type: Boolean, default: true },
state: { type: String, default: "" },
itemUrls: { type: Object, required: true },
},
computed: {
displayButtonTitle() {
@@ -67,7 +71,7 @@ export default {
return "Display";
},
displayDisabled() {
return ["discarded", "new", "upload"].includes(this.state);
return ["discarded", "new", "upload", "queued"].includes(this.state);
},
editButtonTitle() {
if (this.editDisabled) {
@@ -78,6 +82,12 @@ export default {
editDisabled() {
return ["discarded", "new", "upload", "queued", "running", "waiting"].includes(this.state);
},
displayUrl() {
return prependPath(this.itemUrls.display);
},
editUrl() {
return prependPath(this.itemUrls.edit);
},
},
};
</script>
@@ -2,7 +2,14 @@
<div class="dataset-actions mb-1">
<div class="clearfix">
<div class="btn-group float-left">
<b-button v-if="showError" class="px-1" title="Error" size="sm" variant="link" @click.stop="onError">
<b-button
v-if="showError"
class="px-1"
title="Error"
size="sm"
variant="link"
:href="reportErrorUrl"
@click.prevent.stop="onError">
<span class="fa fa-bug" />
</b-button>
<dataset-download v-if="showDownloads" :item="item" @on-download="onDownload" />
@@ -21,7 +28,8 @@
title="Dataset Details"
size="sm"
variant="link"
@click.stop="onInfo">
:href="showDetailsUrl"
@click.prevent.stop="onInfo">
<span class="fa fa-info-circle" />
</b-button>
<b-button
@@ -30,7 +38,8 @@
title="Run Job Again"
size="sm"
variant="link"
@click.stop="onRerun">
:href="rerunUrl"
@click.prevent.stop="onRerun">
<span class="fa fa-redo" />
</b-button>
<b-button
@@ -39,7 +48,8 @@
title="Visualize"
size="sm"
variant="link"
@click.stop="onVisualize">
:href="visualizeUrl"
@click.prevent.stop="onVisualize">
<span class="fa fa-bar-chart-o" />
</b-button>
<b-button
@@ -60,11 +70,11 @@
</template>
<script>
import { iframeAdd } from "components/plugins/legacyNavigation";
import { copy as sendToClipboard } from "utils/clipboard";
import { absPath } from "utils/redirect";
import { absPath, prependPath } from "utils/redirect.js";
import { downloadUrlMixin } from "./mixins.js";
import DatasetDownload from "./DatasetDownload";
import { iframeAdd } from "components/plugins/legacyNavigation";
export default {
components: {
@@ -74,6 +84,7 @@ export default {
props: {
item: { type: Object, required: true },
showHighlight: { type: Boolean, default: false },
itemUrls: { type: Object, required: true },
},
computed: {
showDownloads() {
@@ -97,6 +108,18 @@ export default {
// TODO: Check hasViz, if visualizations are activated in the config
return !this.item.purged && ["ok", "failed_metadata", "error"].includes(this.item.state);
},
reportErrorUrl() {
return prependPath(this.itemUrls.reportError);
},
showDetailsUrl() {
return prependPath(this.itemUrls.showDetails);
},
rerunUrl() {
return prependPath(this.itemUrls.rerun);
},
visualizeUrl() {
return prependPath(this.itemUrls.visualize);
},
},
methods: {
onCopyLink() {
@@ -107,19 +130,18 @@ export default {
window.location.href = resource;
},
onError() {
this.$router.push(`/datasets/${this.item.id}/error`);
this.$router.push(this.itemUrls.reportError);
},
onInfo() {
this.$router.push(`/datasets/${this.item.id}/details`);
this.$router.push(this.itemUrls.showDetails);
},
onRerun() {
this.$router.push(`/root?job_id=${this.item.creating_job}`);
},
onVisualize() {
const name = this.item.name || "...";
iframeAdd({
title: `Visualization of ${name}`,
path: `/visualizations?dataset_id=${this.item.id}`,
path: this.itemUrls.visualize,
title: `Visualization of ${this.item.name || ""}`,
$router: this.$router,
});
},
@@ -21,7 +21,11 @@
<span class="value">{{ result.misc_info }}</span>
</div>
</div>
<DatasetActions :item="result" :show-highlight="showHighlight" @toggleHighlights="toggleHighlights" />
<DatasetActions
:item="result"
:show-highlight="showHighlight"
:item-urls="itemUrls"
@toggleHighlights="toggleHighlights" />
<pre v-if="result.peek" class="dataset-peek p-1" v-html="result.peek" />
</div>
</div>
@@ -94,6 +98,7 @@ export default {
props: {
dataset: { type: Object, required: true },
showHighlight: { type: Boolean, default: false },
itemUrls: { type: Object, required: true },
},
computed: {
stateText() {
@@ -14,12 +14,15 @@
<template v-slot:button-content>
<span class="fa fa-save" />
</template>
<b-dropdown-item v-localize @click.stop="onDownload(downloadUrl)"> Download Dataset </b-dropdown-item>
<b-dropdown-item v-localize :href="downloadUrl" @click.prevent.stop="onDownload(downloadUrl)">
Download Dataset
</b-dropdown-item>
<b-dropdown-item
v-for="(metaFile, index) of metaFiles"
:key="index"
:data-description="`download ${metaFile.file_type}`"
@click.stop="onDownload(metaDownloadUrl, metaFile.file_type)">
:href="metaDownloadUrl + metaFile.file_type"
@click.prevent.stop="onDownload(metaDownloadUrl, metaFile.file_type)">
Download {{ metaFile.file_type }}
</b-dropdown-item>
</b-dropdown>
@@ -29,7 +32,8 @@
title="Download"
size="sm"
variant="link"
@click.stop="onDownload(downloadUrl)">
:href="downloadUrl"
@click.prevent.stop="onDownload(downloadUrl)">
<span class="fa fa-save" />
</b-button>
</template>
@@ -3,8 +3,8 @@
<loading-span v-if="loading" message="Loading dataset" />
<div v-else>
<ContentItem
is-history-item
:id="item.hid"
is-history-item
:item="item"
:name="item.name"
:expand-dataset="expandDataset"
@@ -1,5 +1,5 @@
<template>
<Details
<DetailsLayout
:name="dsc.name"
:tags="dsc.tags"
:writeable="writeable"
@@ -13,19 +13,19 @@
:element-count="dsc.element_count"
:elements-datatypes="dsc.elements_datatypes" />
</template>
</Details>
</DetailsLayout>
</template>
<script>
import short from "components/directives/v-short";
import Details from "components/History/Layout/Details";
import DetailsLayout from "components/History/Layout/DetailsLayout";
import CollectionDescription from "components/History/Content/Collection/CollectionDescription";
import { JobStateSummary } from "components/History/Content/Collection/JobStateSummary";
export default {
components: {
CollectionDescription,
Details,
DetailsLayout,
},
directives: {
short,
@@ -20,7 +20,7 @@
</section>
<section class="position-relative flex-grow-1 scroller">
<div>
<Listing :items="payload" :loading="loading" @scroll="onScroll">
<ListingLayout :items="payload" :loading="loading" @scroll="onScroll">
<template v-slot:item="{ item }">
<ContentItem
:id="item.element_index + 1"
@@ -31,7 +31,7 @@
@update:expand-dataset="setExpanded(item, $event)"
@view-collection="onViewSubCollection" />
</template>
</Listing>
</ListingLayout>
</div>
</section>
</section>
@@ -47,7 +47,7 @@ import CollectionNavigation from "./CollectionNavigation";
import CollectionOperations from "./CollectionOperations";
import CollectionDetails from "./CollectionDetails";
import ExpandedItems from "components/History/Content/ExpandedItems";
import Listing from "components/History/Layout/Listing";
import ListingLayout from "components/History/Layout/ListingLayout";
export default {
components: {
@@ -57,7 +57,7 @@ export default {
CollectionOperations,
ContentItem,
ExpandedItems,
Listing,
ListingLayout,
},
props: {
history: { type: Object, required: true },
@@ -83,6 +83,14 @@ export default {
return this.dsc.contents_url.substring(1);
},
},
watch: {
history(newHistory, oldHistory) {
if (newHistory.id != oldHistory.id) {
// Send up event closing out selected collection on history change.
this.$emit("update:selected-collections", []);
}
},
},
methods: {
updateDsc(collection, fields) {
updateContentFields(collection, fields).then((response) => {
@@ -45,8 +45,8 @@
</b-button>
<b-button
v-b-tooltip.hover
:title="'Last refreshed ' + diffToNow"
variant="link"
:title="reloadButtonTitle"
:variant="reloadButtonVariant"
size="sm"
class="rounded-0 text-decoration-none"
@click="reloadContents()">
@@ -70,12 +70,14 @@ export default {
mixins: [usesDetailedHistoryMixin],
props: {
history: { type: Object, required: true },
isWatching: { type: Boolean, default: false },
lastChecked: { type: Date, default: null },
},
data() {
return {
diffToNow: 0,
reloadButtonCls: "fa fa-sync",
reloadButtonTitle: "",
reloadButtonVariant: "link",
};
},
mounted() {
@@ -91,7 +93,16 @@ export default {
this.$emit("update:filter-text", newFilterText);
},
updateTime() {
this.diffToNow = formatDistanceToNowStrict(this.lastChecked, { addSuffix: true, includeSeconds: true });
const diffToNow = formatDistanceToNowStrict(this.lastChecked, { addSuffix: true, includeSeconds: true });
const diffToNowSec = Date.now() - this.lastChecked;
// if history isn't being watched or hasn't been watched/polled for over 2 minutes
if (!this.isWatching || diffToNowSec > 120000) {
this.reloadButtonTitle = "Last refreshed " + diffToNow + ". Consider reloading the page.";
this.reloadButtonVariant = "danger";
} else {
this.reloadButtonTitle = "Last refreshed " + diffToNow;
this.reloadButtonVariant = "link";
}
},
async reloadContents() {
this.$emit("reloadContents");
@@ -1,5 +1,5 @@
<template>
<Details
<DetailsLayout
:name="history.name"
:annotation="history.annotation"
:tags="history.tags"
@@ -8,16 +8,16 @@
<template v-slot:name>
<h3 v-short="history.name || 'History'" data-description="name display" class="my-2" />
</template>
</Details>
</DetailsLayout>
</template>
<script>
import short from "components/directives/v-short";
import Details from "components/History/Layout/Details";
import DetailsLayout from "components/History/Layout/DetailsLayout";
export default {
components: {
Details,
DetailsLayout,
},
directives: {
short,
@@ -8,6 +8,7 @@
<b-button-group>
<b-button
v-b-tooltip.bottom.hover
class="create-hist-btn"
data-description="create new history"
size="sm"
variant="link"
@@ -31,7 +32,6 @@
size="sm"
variant="link"
toggle-class="text-decoration-none"
title="Show history options"
menu-class="history-options-button-menu"
data-description="history options">
<b-dropdown-text>
@@ -43,8 +43,9 @@
</b-dropdown-text>
<b-dropdown-item
v-if="!currentUser.isAnonymous"
data-description="switch to multi history view"
:disabled="currentUser.isAnonymous"
:title="userTitle('Open History Multiview')"
@click="redirect('/history/view_multiple')">
<Icon fixed-width class="mr-1" icon="columns" />
<span v-localize>Show Histories Side-by-Side</span>
@@ -52,75 +53,91 @@
<b-dropdown-divider></b-dropdown-divider>
<b-dropdown-item v-if="!currentUser.isAnonymous" v-b-modal:copy-history-modal>
<Icon fixed-width icon="copy" class="mr-1" />
<span v-localize>Copy this History</span>
</b-dropdown-item>
<b-dropdown-item v-b-modal:delete-history-modal>
<Icon fixed-width icon="trash" class="mr-1" />
<span v-localize>Delete this History</span>
</b-dropdown-item>
<b-dropdown-divider></b-dropdown-divider>
<b-dropdown-item @click="iframeRedirect('/history/resume_paused_jobs?current=True')">
<b-dropdown-item
:title="l('Resume all Paused Jobs in this History')"
@click="iframeRedirect('/history/resume_paused_jobs?current=True')">
<Icon fixed-width icon="play" class="mr-1" />
<span v-localize>Resume Paused Jobs</span>
</b-dropdown-item>
<b-dropdown-item
v-if="!currentUser.isAnonymous"
@click="iframeRedirect('/workflow/build_from_current_history')">
<Icon fixed-width icon="file-export" class="mr-1" />
<span v-localize>Extract Workflow</span>
</b-dropdown-item>
<b-dropdown-item
data-description="show structure"
:title="l('Show Detailed Structure View of History')"
@click="$router.push('/histories/show_structure')">
<Icon fixed-width icon="code-branch" class="mr-1" />
<span v-localize>Show Structure</span>
</b-dropdown-item>
<template v-if="!currentUser.isAnonymous">
<b-dropdown-divider></b-dropdown-divider>
<b-dropdown-item
data-description="share or publish"
@click="$router.push(`/histories/sharing?id=${history.id}`)">
<Icon fixed-width icon="share-alt" class="mr-1" />
<span v-localize>Share or Publish</span>
</b-dropdown-item>
<b-dropdown-item @click="$router.push(`/histories/permissions?id=${history.id}`)">
<Icon fixed-width icon="user-lock" class="mr-1" />
<span v-localize>Set Permissions</span>
</b-dropdown-item>
<b-dropdown-item v-b-modal:history-privacy-modal>
<Icon fixed-width icon="lock" class="mr-1" />
<span v-localize>Make Private</span>
</b-dropdown-item>
</template>
<b-dropdown-divider></b-dropdown-divider>
<b-dropdown-item @click="$router.push(`/histories/citations?id=${history.id}`)">
<b-dropdown-item
v-b-modal:copy-history-modal
:disabled="currentUser.isAnonymous"
:title="userTitle('Copy History to a New History')">
<Icon fixed-width icon="copy" class="mr-1" />
<span v-localize>Copy this History</span>
</b-dropdown-item>
<b-dropdown-item v-b-modal:delete-history-modal :title="l('Permanently Delete History')">
<Icon fixed-width icon="trash" class="mr-1" />
<span v-localize>Delete this History</span>
</b-dropdown-item>
<b-dropdown-item
:title="l('Export Citations for all Tools used in this History')"
@click="$router.push(`/histories/citations?id=${history.id}`)">
<Icon fixed-width icon="stream" class="mr-1" />
<span v-localize>Export Tool Citations</span>
</b-dropdown-item>
<b-dropdown-item
data-description="export to file"
:title="l('Export and Download History as a File')"
@click="$router.push(`/histories/${history.id}/export`)">
<Icon fixed-width icon="file-archive" class="mr-1" />
<span v-localize>Export History to File</span>
</b-dropdown-item>
<b-dropdown-item
:disabled="currentUser.isAnonymous"
:title="userTitle('Convert History to Workflow')"
@click="iframeRedirect('/workflow/build_from_current_history')">
<Icon fixed-width icon="file-export" class="mr-1" />
<span v-localize>Extract Workflow</span>
</b-dropdown-item>
<b-dropdown-divider></b-dropdown-divider>
<b-dropdown-item
:disabled="currentUser.isAnonymous"
:title="userTitle('Share or Publish this History')"
data-description="share or publish"
@click="$router.push(`/histories/sharing?id=${history.id}`)">
<Icon fixed-width icon="share-alt" class="mr-1" />
<span v-localize>Share or Publish</span>
</b-dropdown-item>
<b-dropdown-item
:disabled="currentUser.isAnonymous"
:title="userTitle('Set who can View or Edit this History')"
@click="$router.push(`/histories/permissions?id=${history.id}`)">
<Icon fixed-width icon="user-lock" class="mr-1" />
<span v-localize>Set Permissions</span>
</b-dropdown-item>
<b-dropdown-item
v-b-modal:history-privacy-modal
:disabled="currentUser.isAnonymous"
:title="userTitle('Make this History Private')">
<Icon fixed-width icon="lock" class="mr-1" />
<span v-localize>Make Private</span>
</b-dropdown-item>
<b-dropdown-divider></b-dropdown-divider>
<b-dropdown-item
data-description="switch to legacy history view"
:title="l('Switch to Legacy History Panel')"
@click="switchToLegacyHistoryPanel">
<Icon fixed-width class="mr-1" icon="arrow-up" />
<span v-localize>Return to legacy panel</span>
@@ -187,6 +204,13 @@ export default {
},
methods: {
switchToLegacyHistoryPanel,
userTitle(title) {
if (this.currentUser.isAnonymous) {
return this.l("Log in to") + " " + this.l(title);
} else {
return this.l(title);
}
},
},
};
</script>
@@ -13,7 +13,7 @@
<b-dropdown-text id="history-op-all-content">
<span v-localize>With entire history...</span>
</b-dropdown-text>
<b-dropdown-item v-if="numItemsActive" data-description="copy datasets" @click="onCopy">
<b-dropdown-item data-description="copy datasets" @click="onCopy">
<span v-localize>Copy Datasets</span>
</b-dropdown-item>
<b-dropdown-item v-if="numItemsHidden" v-b-modal:show-all-hidden-content>
@@ -38,6 +38,7 @@
<HistoryMessages :history="history" />
<HistoryCounter
:history="history"
:is-watching="isWatching"
:last-checked="lastChecked"
:filter-text.sync="filterText"
@reloadContents="reloadContents" />
@@ -91,7 +92,7 @@
No data found for selected filter.
</b-alert>
</div>
<Listing
<ListingLayout
v-else
:offset="listOffset"
:items="itemsLoaded"
@@ -100,8 +101,8 @@
<template v-slot:item="{ item, currentOffset }">
<ContentItem
v-if="!invisible[item.hid]"
is-history-item
:id="item.hid"
is-history-item
:item="item"
:name="item.name"
:expand-dataset="isExpanded(item)"
@@ -119,7 +120,7 @@
@undelete="onUndelete(item)"
@unhide="onUnhide(item)" />
</template>
</Listing>
</ListingLayout>
</div>
</section>
</section>
@@ -137,7 +138,7 @@ import { deleteContent, updateContentFields } from "components/History/model/que
import { getHighlights } from "components/History/Content/model/highlights";
import ExpandedItems from "components/History/Content/ExpandedItems";
import SelectedItems from "components/History/Content/SelectedItems";
import Listing from "components/History/Layout/Listing";
import ListingLayout from "components/History/Layout/ListingLayout";
import HistoryCounter from "./HistoryCounter";
import HistoryOperations from "./HistoryOperations/Index";
import HistoryDetails from "./HistoryDetails";
@@ -164,7 +165,7 @@ export default {
HistorySelectionOperations,
HistorySelectionStatus,
LoadingSpan,
Listing,
ListingLayout,
SelectedItems,
SelectionChangeWarning,
OperationErrorDialog,
@@ -211,6 +212,9 @@ export default {
lastChecked() {
return this.$store.getters.getLastCheckedTime();
},
isWatching() {
return this.$store.getters.getWatchingVisibility();
},
},
watch: {
queryKey() {
@@ -1,12 +1,12 @@
<template>
<section class="m-3 details" data-description="edit details">
<b-button
v-if="!currentUser.isAnonymous && writeable"
:disabled="currentUser.isAnonymous || !writeable"
class="edit-button ml-1 float-right"
data-description="editor toggle"
size="sm"
variant="link"
:title="'Edit' | l"
:title="editButtonTitle"
:pressed="editing"
@click="onToggle">
<Icon icon="pen" />
@@ -91,6 +91,17 @@ export default {
},
computed: {
...mapGetters("user", ["currentUser"]),
editButtonTitle() {
if (this.currentUser?.isAnonymous) {
return this.l("Log in to Rename History");
} else {
if (this.writeable) {
return this.l("Edit");
} else {
return this.l("Not Editable");
}
}
},
},
methods: {
onSave() {
@@ -3,7 +3,7 @@
<transition name="fade">
<b-alert v-localize :show="currentUser.isAnonymous" variant="warning">
As an anonymous user, unless you login or register, you will lose your current history after copying
this history. You can <a href="/user/login">login here</a> or <a href="/user/create">register here</a>.
this history. You can <a href="/user/login">log in here</a> or <a href="/user/create">register here</a>.
</b-alert>
</transition>
@@ -1,6 +1,6 @@
<template>
<div>
<job-details-provider auto-refresh :jobId="job_id" @update:result="updateJob" />
<job-details-provider auto-refresh :job-id="job_id" @update:result="updateJob" />
<h3>Job Information</h3>
<table id="job-information" class="tabletip info_data_table">
<tbody>
@@ -62,8 +62,10 @@
class="form-control input_library_name"
rows="3" />
<div v-else-if="row.item.deleted && include_deleted" class="deleted-item">{{ row.item.name }}</div>
<b-link v-else :to="{ path: `folders/${row.item.root_folder_id}` }">{{ row.item.name }}</b-link>
<div v-else-if="row.item.deleted && includeDeleted" class="deleted-item">{{ row.item.name }}</div>
<b-link v-else :to="{ path: `/libraries/folders/${row.item.root_folder_id}` }">{{
row.item.name
}}</b-link>
</template>
<template v-slot:cell(description)="{ item }">
<LibraryEditField
@@ -123,7 +125,7 @@
size="sm"
class="lib-btn permission_library_btn"
:title="'Permissions of ' + row.item.name"
:to="{ path: `/${row.item.id}/permissions` }">
:to="{ path: `/libraries/${row.item.id}/permissions` }">
<font-awesome-icon icon="users" />
Manage
</b-button>
@@ -212,7 +214,7 @@ export default {
perPage: DEFAULT_PER_PAGE,
librariesList: [],
maxDescriptionLength: MAX_DESCRIPTION_LENGTH,
include_deleted: false,
includeDeleted: false,
exclude_restricted: false,
filterOn: [],
excluded: [],
@@ -243,11 +245,11 @@ export default {
created() {
this.root = getAppRoot();
this.services = new Services({ root: this.root });
this.loadLibraries(this.include_deleted);
this.loadLibraries(this.includeDeleted);
},
methods: {
loadLibraries(include_deleted = false) {
this.services.getLibraries(include_deleted).then((result) => (this.librariesList = result));
loadLibraries(includeDeleted = false) {
this.services.getLibraries(includeDeleted).then((result) => (this.librariesList = result));
},
toggleEditMode(item) {
item.editMode = !item.editMode;
@@ -282,7 +284,7 @@ export default {
Toast.success("Library has been marked deleted.");
deletedLib.deleted = true;
this.toggleEditMode(deletedLib);
if (!this.include_deleted) {
if (!this.includeDeleted) {
this.hideOn("deleted", false);
}
},
@@ -301,9 +303,9 @@ export default {
this.filter = value;
},
toggle_include_deleted(isDeletedIncluded) {
this.include_deleted = isDeletedIncluded;
if (this.include_deleted) {
this.services.getLibraries(this.include_deleted).then((result) => {
this.includeDeleted = isDeletedIncluded;
if (this.includeDeleted) {
this.services.getLibraries(this.includeDeleted).then((result) => {
this.librariesList = this.librariesList.concat(result);
this.$refs.libraries_list.refresh();
});
@@ -1,12 +1,14 @@
<template>
<b-breadcrumb>
<b-breadcrumb-item title="Return to the list of libraries" :to="{ path: `/` }"> Libraries </b-breadcrumb-item>
<b-breadcrumb-item title="Return to the list of libraries" :to="{ path: `/libraries` }">
Libraries
</b-breadcrumb-item>
<template v-for="path_item in full_path">
<b-breadcrumb-item
:key="path_item[0]"
:title="isCurrentId(path_item[0]) ? `You are here` : `Return to this folder`"
:active="isCurrentId(path_item[0])"
:to="{ path: `/folders/${path_item[0]}` }"
:to="{ path: `/libraries/folders/${path_item[0]}` }"
href="#"
>{{ path_item[1] }}</b-breadcrumb-item
>
@@ -3,8 +3,8 @@
<div>
<FolderTopBar
:folder-contents="folderContents"
:include_deleted="include_deleted"
:folder_id="current_folder_id"
:include-deleted.sync="includeDeleted"
:folder_id="currentFolderId"
:selected="selected"
:metadata="folder_metadata"
:unselected="unselected"
@@ -29,6 +29,7 @@
selectable
no-select-on-click
show-empty
@sort-changed="onSort"
@row-clicked="onRowClick">
<template v-slot:empty>
<div v-if="isBusy" class="text-center my-2">
@@ -292,8 +293,7 @@ function initialFolderState() {
unselected: [],
expandedMessage: [],
folderContents: [],
include_deleted: false,
search_text: "",
includeDeleted: false,
isAllSelectedMode: false,
};
}
@@ -323,8 +323,11 @@ export default {
return {
...initialFolderState(),
...{
currentPage: null,
current_folder_id: null,
currentPage: 1,
sortBy: "name",
sortDesc: false,
searchText: "",
currentFolderId: null,
error: null,
isBusy: false,
folder_metadata: {},
@@ -338,10 +341,17 @@ export default {
};
},
watch: {
perPage: {
handler: function (value) {
this.fetchFolderContents(this.include_deleted);
},
perPage() {
this.fetchFolderContents();
},
includeDeleted() {
this.fetchFolderContents();
},
sortBy() {
this.fetchFolderContents();
},
sortDesc() {
this.fetchFolderContents();
},
},
created() {
@@ -350,25 +360,30 @@ export default {
},
methods: {
getFolder(folder_id, page) {
this.current_folder_id = folder_id;
this.currentFolderId = folder_id;
this.currentPage = page;
this.resetData();
this.fetchFolderContents(this.include_deleted);
this.fetchFolderContents();
},
resetData() {
const data = initialFolderState();
Object.keys(data).forEach((k) => (this[k] = data[k]));
},
fetchFolderContents(include_deleted = false) {
this.include_deleted = include_deleted;
onSort(props) {
this.sortBy = props.sortBy;
this.sortDesc = props.sortDesc;
},
fetchFolderContents() {
this.setBusy(true);
this.services
.getFolderContents(
this.current_folder_id,
include_deleted,
this.currentFolderId,
this.includeDeleted,
this.sortBy,
this.sortDesc,
this.perPage,
(this.currentPage - 1) * this.perPage,
this.search_text
this.searchText
)
.then((response) => {
this.folderContents = response.folder_contents;
@@ -391,9 +406,9 @@ export default {
});
},
updateSearchValue(value) {
this.search_text = value;
this.searchText = value;
this.folderContents = [];
this.fetchFolderContents(this.include_deleted);
this.fetchFolderContents();
},
selectAllRenderedRows() {
this.$refs.folder_content_table.items.forEach((row, index) => {
@@ -413,7 +428,7 @@ export default {
this.$refs.folder_content_table.refresh();
},
refreshTableContent() {
this.fetchFolderContents(this.include_deleted);
this.fetchFolderContents();
},
deleteFromTable(deletedItem) {
this.folderContents = this.folderContents.filter((element) => {
@@ -495,9 +510,9 @@ export default {
},
navigateToPermission(element) {
if (element.type === "file") {
return `/folders/${this.folder_id}/dataset/${element.id}/permissions`;
return `/libraries/folders/${this.folder_id}/dataset/${element.id}/permissions`;
} else if (element.type === "folder") {
return `/folders/${element.id}/permissions`;
return `/libraries/folders/${element.id}/permissions`;
}
},
getMessage(element) {
@@ -533,7 +548,7 @@ export default {
} else {
this.services.newFolder(
{
parent_id: this.current_folder_id,
parent_id: this.currentFolderId,
name: folder.name,
description: folder.description,
},
@@ -3,7 +3,7 @@
<div class="form-inline d-flex align-items-center mb-2">
<b-button
class="mr-1 btn btn-secondary"
:to="{ path: `/` }"
:to="{ path: `/libraries` }"
data-toggle="tooltip"
title="Go to libraries list">
<font-awesome-icon icon="home" />
@@ -61,7 +61,7 @@
class="primary-button dropdown-toggle add-to-history"
data-toggle="dropdown">
<font-awesome-icon icon="book" />
Export to History <span class="caret"></span>
Add to History <span class="caret"></span>
</button>
<div class="dropdown-menu" role="menu">
<a
@@ -101,11 +101,7 @@
</button>
<FolderDetails :id="folder_id" class="mr-1" :metadata="metadata" />
<div v-if="canDelete" class="form-check logged-dataset-manipulation mr-1">
<b-form-checkbox
id="checkbox-1"
:checked="include_deleted"
name="checkbox-1"
@input="toggle_include_deleted($event)">
<b-form-checkbox :checked="includeDeleted" @change="$emit('update:includeDeleted', $event)">
include deleted
</b-form-checkbox>
</div>
@@ -155,7 +151,7 @@ export default {
type: String,
required: true,
},
include_deleted: {
includeDeleted: {
type: Boolean,
required: true,
},
@@ -244,7 +240,7 @@ export default {
const selected = await this.services.getFilteredFolderContents(
this.folder_id,
this.unselected,
this.$parent.search_text
this.$parent.searchText
);
this.$emit("setBusy", false);
return selected;
@@ -341,9 +337,6 @@ export default {
cache: true,
});
},
toggle_include_deleted: function (value) {
this.$emit("fetchFolderContents", value);
},
updateContent: function () {
this.$emit("fetchFolderContents");
},
@@ -7,29 +7,38 @@ export class Services {
this.root = options.root || getAppRoot();
}
async getFolderContents(id, include_deleted, limit, offset, search_text = false) {
const url = `${
this.root
}api/folders/${id}/contents?include_deleted=${include_deleted}&limit=${limit}&offset=${offset}${this.getSearchQuery(
search_text
)}`;
async getFolderContents(folderId, includeDeleted, sortBy, sortDesc, limit, offset, searchText) {
const url = `${this.root}api/folders/${folderId}/contents`;
const config = {
params: {
include_deleted: includeDeleted,
sort_by: sortBy,
sort_desc: sortDesc,
limit,
offset,
},
};
searchText = searchText.trim();
if (searchText) {
config.params.search_text = searchText;
}
try {
const response = await axios.get(url);
const response = await axios.get(url, config);
return response.data;
} catch (e) {
rethrowSimple(e);
}
}
async getFilteredFolderContents(id, excluded, search_text) {
const contents = await axios.get(`${this.root}api/folders/${id}/contents?${this.getSearchQuery(search_text)}`);
async getFilteredFolderContents(id, excluded, searchText) {
const contents = await axios.get(`${this.root}api/folders/${id}/contents?${this.getSearchQuery(searchText)}`);
return contents.data.folder_contents.filter((item) => {
return !excluded.some((exc) => exc.id === item.id);
});
}
getSearchQuery(search_text) {
return search_text ? `&search_text=${encodeURI(search_text.trim())}` : "";
getSearchQuery(searchText) {
return searchText ? `&search_text=${encodeURI(searchText.trim())}` : "";
}
updateFolder(item, onSucess, onError) {
@@ -16,7 +16,7 @@ export const fields = [
{
label: "Description",
key: "message",
sortable: false,
sortable: true,
},
{
label: "Type",
@@ -36,7 +36,7 @@ export const fields = [
{
label: "State",
key: "state",
sortable: true,
sortable: false,
},
{
label: "",
@@ -1,61 +0,0 @@
import { getAppRoot } from "onload/loadConfig";
import Vue from "vue";
import VueRouter from "vue-router";
import LibraryFolderPermissions from "components/Libraries/LibraryFolder/LibraryFolderPermissions/LibraryFolderPermissions.vue";
import LibraryDataset from "components/Libraries/LibraryFolder/LibraryFolderDataset/LibraryDataset.vue";
import LibraryFolder from "components/Libraries/LibraryFolder/LibraryFolder.vue";
import LibraryFolderDatasetPermissions from "components/Libraries/LibraryFolder/LibraryFolderPermissions/LibraryFolderDatasetPermissions.vue";
import LibrariesList from "components/Libraries/LibrariesList.vue";
import LibraryPermissions from "components/Libraries/LibraryPermissions/LibraryPermissions.vue";
Vue.use(VueRouter);
export default new VueRouter({
mode: "history",
base: `${getAppRoot()}libraries`,
routes: [
{
path: "/",
name: "LibrariesList",
component: LibrariesList,
},
{
path: "/:library_id/permissions",
name: "LibraryPermissions",
component: LibraryPermissions,
props: true,
},
// redirect to the 1st page
{ path: "/folders/:folder_id", redirect: "/folders/:folder_id/page/1" },
{
path: "/folders/:folder_id/page/:page",
name: "LibraryFolder",
component: LibraryFolder,
props(route) {
const props = { ...route.params };
if (props.page) {
props.page = +props.page;
}
return props;
},
},
{
path: "/folders/:folder_id/dataset/:dataset_id",
name: "LibraryDataset",
component: LibraryDataset,
props: true,
},
{
path: "/folders/:folder_id/permissions",
name: "LibraryFolder",
component: LibraryFolderPermissions,
props: true,
},
{
path: "/folders/:folder_id/dataset/:dataset_id/permissions",
name: "LibraryFolderDatasetPermissions",
component: LibraryFolderDatasetPermissions,
props: true,
},
],
});
+1 -1
View File
@@ -39,7 +39,7 @@ const tableIcons = [
const manageIcons = [faAngleDoubleLeft, faSave, faFile];
const topBarIcons = [faHome, faPlus, faTrash, faDownload, faBook];
const librariesIcons = [faGlobe, faPencilAlt, faSave, faTimes, faTrash, faUnlock];
const librariesIcons = [faGlobe, faPencilAlt, faSave, faTimes, faTrash, faUsers, faHome, faUnlock];
export function initFolderTableIcons() {
tableIcons.forEach((icon) => library.add(icon));
-8
View File
@@ -1,8 +0,0 @@
import router from "./LibraryRouter.js";
export const Libraries = {
router,
render: (h) => h("router-view"),
};
export default Libraries;
+2 -2
View File
@@ -7,8 +7,8 @@ export class Services {
this.root = options.root || getAppRoot();
}
async getLibraries(include_deleted = false) {
const url = `${this.root}api/libraries?deleted=${include_deleted}`;
async getLibraries(includeDeleted = false) {
const url = `${this.root}api/libraries?deleted=${includeDeleted}`;
try {
const response = await axios.get(url);
return response.data;
+10 -14
View File
@@ -1,41 +1,37 @@
<template>
<loading-span v-if="license == null" message="Loading license information"> </loading-span>
<div v-else-if="license.name" class="text-muted">
<span v-else-if="license.name" class="text-muted">
<link itemprop="license" :href="license.licenseId" />
<span v-if="title">
{{ title }}
</span>
{{ license.name }}
<a target="_blank" :href="license.url">
<font-awesome-icon icon="external-link-alt" />
</a>
<external-link :href="license.url">
{{ license.name }}
</external-link>
<slot name="buttons"></slot>
</div>
<div v-else>
</span>
<span v-else>
Unknown License (<i>{{ license.url }}</i
>)
<slot name="buttons"></slot>
</div>
</span>
</template>
<script>
import { getAppRoot } from "onload/loadConfig";
import axios from "axios";
import LoadingSpan from "components/LoadingSpan";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { library } from "@fortawesome/fontawesome-svg-core";
import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons";
library.add(faExternalLinkAlt);
import ExternalLink from "components/ExternalLink";
export default {
components: {
LoadingSpan,
FontAwesomeIcon,
ExternalLink,
},
props: {
licenseId: {
type: String,
required: true,
},
inputLicenseInfo: {
type: Object,
+24 -17
View File
@@ -1,9 +1,12 @@
import Vuex from "vuex";
import { default as Masthead } from "./Masthead.vue";
import { mount } from "@vue/test-utils";
import { getLocalVue } from "jest/helpers";
import { WindowManager } from "layout/window-manager";
import { fetchMenu } from "layout/menu";
import { loadWebhookMenuItems } from "./_webhooks";
import { userStore } from "store/userStore";
import { configStore } from "store/configStore";
jest.mock("app");
jest.mock("layout/menu");
@@ -13,9 +16,10 @@ describe("Masthead.vue", () => {
let wrapper;
let localVue;
let windowManager;
let quotaRendered;
let quotaEl;
let tabs;
let store;
let state;
let actions;
function stubFetchMenu() {
return tabs;
@@ -35,17 +39,25 @@ describe("Masthead.vue", () => {
beforeEach(() => {
localVue = getLocalVue();
quotaRendered = false;
quotaEl = null;
const quotaMeter = {
setElement: function (el) {
quotaEl = el;
store = new Vuex.Store({
modules: {
user: {
state,
actions: {
loadUser: jest.fn(),
},
getters: userStore.getters,
namespaced: true,
},
config: {
state,
actions,
getters: configStore.getters,
namespaced: true,
},
},
render: function () {
quotaRendered = true;
},
};
});
tabs = [
// Main Analysis Tab..
@@ -78,7 +90,6 @@ describe("Masthead.vue", () => {
};
windowManager = new WindowManager({});
const mastheadState = {
quotaMeter,
windowManager,
};
@@ -87,6 +98,7 @@ describe("Masthead.vue", () => {
mastheadState,
initialActiveTab,
},
store,
localVue,
});
});
@@ -99,11 +111,6 @@ describe("Masthead.vue", () => {
expect(wrapper.find(".navbar-brand-title").text()).toBe("Foo");
});
it("set quota element and renders it", () => {
expect(quotaEl).not.toBeNull();
expect(quotaRendered).toBe(true);
});
it("should render simple tab item links", () => {
expect(wrapper.findAll("li.nav-item").length).toBe(5);
// Ensure specified link title respected.
+3 -5
View File
@@ -14,7 +14,7 @@
:active-tab="activeTab" />
<masthead-item :tab="windowTab" :toggle="windowToggle" @click="onWindowToggle" />
</b-navbar-nav>
<div ref="quota-meter-container" class="quota-meter-container" />
<quota-meter />
</b-navbar>
</template>
@@ -23,6 +23,7 @@ import { BNavbar, BNavbarBrand, BNavbarNav } from "bootstrap-vue";
import MastheadItem from "./MastheadItem";
import { fetchMenu } from "layout/menu";
import { loadWebhookMenuItems } from "./_webhooks";
import QuotaMeter from "./QuotaMeter.vue";
export default {
name: "Masthead",
@@ -31,6 +32,7 @@ export default {
BNavbarBrand,
BNavbarNav,
MastheadItem,
QuotaMeter,
},
props: {
displayGalaxyBrand: {
@@ -92,10 +94,6 @@ export default {
this.baseTabs = fetchMenu(this.menuOptions);
loadWebhookMenuItems(this.extensionTabs);
},
mounted() {
this.mastheadState.quotaMeter.setElement(this.$refs["quota-meter-container"]);
this.mastheadState.quotaMeter.render();
},
methods: {
addItem(item) {
this.tabs.push(item);
@@ -84,7 +84,7 @@ export default {
return this.tab.menu;
},
popoverNote() {
return `Please <a href="${getAppRoot()}login">login or register</a> to use this feature.`;
return `Please <a href="${getAppRoot()}login">log in or register</a> to use this feature.`;
},
classes() {
const isActiveTab = this.tab.id == this.activeTab;
@@ -143,8 +143,6 @@ export default {
const Galaxy = getGalaxyInstance();
if (tab.target === "__use_router__" && this.$router) {
this.$router.push(`/${tab.url}`);
} else if (tab.target === "__use_router__" && typeof Galaxy.page !== "undefined") {
Galaxy.page.router.executeUseRouter(this.formatUrl(tab.url));
} else {
try {
Galaxy.frame.add({ ...tab, url: this.formatUrl(tab.url) });
@@ -0,0 +1,118 @@
import Vuex from "vuex";
import QuotaMeter from "./QuotaMeter.vue";
import { mount } from "@vue/test-utils";
import { getLocalVue } from "jest/helpers";
import { userStore } from "store/userStore";
import { configStore } from "store/configStore";
const localVue = getLocalVue();
const createStore = (currentUser, config) => {
return new Vuex.Store({
modules: {
config: {
state: {
config,
},
getters: configStore.getters,
namespaced: true,
},
user: {
state: {
currentUser,
},
actions: {
loadUser: jest.fn(),
},
getters: userStore.getters,
namespaced: true,
},
},
});
};
describe("QuotaMeter.vue", () => {
it("shows a percentage usage", () => {
const user = {
total_disk_usage: 5120,
quota_percent: 50,
};
const config = { enable_quotas: true };
const store = createStore(user, config);
const wrapper = mount(QuotaMeter, { localVue, store });
expect(wrapper.find(".quota-progress > span").text()).toBe("Using 50%");
});
it("changes appearance depending on usage", () => {
const config = { enable_quotas: true };
{
const user = { quota_percent: 30 };
const store = createStore(user, config);
const wrapper = mount(QuotaMeter, { localVue, store });
expect(wrapper.find(".quota-progress .progress-bar").classes()).toContain("bg-success");
}
{
const user = { quota_percent: 80 };
const store = createStore(user, config);
const wrapper = mount(QuotaMeter, { localVue, store });
expect(wrapper.find(".quota-progress .progress-bar").classes()).toContain("bg-warning");
}
{
const user = { quota_percent: 95 };
const store = createStore(user, config);
const wrapper = mount(QuotaMeter, { localVue, store });
expect(wrapper.find(".quota-progress .progress-bar").classes()).toContain("bg-danger");
}
});
it("prompts user to log in", () => {
const config = { enable_quotas: true };
const store = createStore({}, config);
const wrapper = mount(QuotaMeter, { localVue, store });
expect(wrapper.find(".quota-meter > a").attributes("title")).toContain("Log in");
});
it("shows total usage in title", () => {
const user = { total_disk_usage: 9216 };
const config = { enable_quotas: true };
const store = createStore(user, config);
const wrapper = mount(QuotaMeter, { localVue, store });
expect(wrapper.find(".quota-progress").attributes("title")).toContain("9 KB");
});
it("shows total usage when there is no quota", () => {
{
const user = { total_disk_usage: 7168 };
const config = { enable_quotas: false };
const store = createStore(user, config);
const wrapper = mount(QuotaMeter, { localVue, store });
expect(wrapper.find(".quota-meter > a").text()).toBe("Using 7 KB");
expect(wrapper.find(".quota-meter > a").attributes("title")).not.toContain("7 KB");
}
{
const user = {
total_disk_usage: 21504,
quota: "unlimited",
};
const config = { enable_quotas: true };
const store = createStore(user, config);
const wrapper = mount(QuotaMeter, { localVue, store });
expect(wrapper.find(".quota-meter > a").text()).toBe("Using 21 KB");
expect(wrapper.find(".quota-meter > a").attributes("title")).not.toContain("21 KB");
}
});
});
@@ -0,0 +1,109 @@
<template>
<current-user>
<div class="quota-meter d-flex align-items-center">
<b-link v-if="!hasQuota" v-b-tooltip.hover.left to="/storage" class="ml-auto quota-text" :title="title">
{{ usingString + " " + totalUsageString }}
</b-link>
<b-link v-else v-b-tooltip.hover.left class="quota-progress" to="/storage" :title="title">
<b-progress :value="usage" :max="100" :variant="variant" />
<span>{{ usingString + " " + usage.toFixed(0) }}%</span>
</b-link>
</div>
</current-user>
</template>
<script>
import { bytesToString } from "utils/utils";
import CurrentUser from "components/providers/CurrentUser";
import { mapGetters } from "vuex";
export default {
name: "QuotaMeter",
components: {
CurrentUser,
},
data() {
return {
usingString: this.l("Using"),
};
},
computed: {
...mapGetters("config", ["config"]),
...mapGetters("user", ["currentUser"]),
hasQuota() {
const quotasEnabled = this.config?.enable_quotas ?? false;
const quotaLimited = this.currentUser?.quota !== "unlimited" ?? false;
return quotasEnabled && quotaLimited;
},
usage() {
if (this.hasQuota) {
return this.currentUser?.quota_percent ?? 0;
} else {
return this.currentUser?.total_disk_usage ?? 0;
}
},
totalUsageString() {
const total = this.currentUser?.total_disk_usage ?? 0;
return bytesToString(total, true);
},
title() {
let details = "";
if (this.currentUser.isAnonymous) {
details = this.l("Log in for details.");
} else {
details = this.l("Click for details.");
}
if (this.hasQuota) {
return this.usingString + " " + this.totalUsageString + ". " + details;
} else {
return details;
}
},
variant() {
if (!this.hasQuota || this.usage < 80) {
return "success";
} else if (this.usage < 95) {
return "warning";
} else {
return "danger";
}
},
},
};
</script>
<style lang="scss" scoped>
@import "theme/blue.scss";
.quota-meter {
position: absolute;
right: 0.8rem;
width: 100px;
height: 100%;
.quota-progress {
width: 100%;
height: 16px;
position: relative;
& > * {
position: absolute;
width: 100%;
height: 100%;
text-align: center;
}
& > span {
line-height: 1em;
pointer-events: none;
}
}
.quota-text {
color: $brand-light;
text-decoration: none;
}
}
</style>
+1 -1
View File
@@ -8,7 +8,7 @@ export default {
props: {
message: {
type: String,
required: true,
default: null,
},
status: {
type: String,
@@ -90,7 +90,7 @@ export default {
</script>
<style scoped type="scss">
.new-user-welcome::v-deep {
.new-user-welcome:deep() {
.carousel-fig {
padding-bottom: 10;
}
@@ -5,6 +5,7 @@
size="sm"
variant="link"
aria-label="Show favorite tools"
:disabled="currentUser.isAnonymous"
:title="tooltipText"
@click="onFavorites">
<icon v-if="toggle" :icon="['fas', 'star']" />
@@ -13,7 +14,7 @@
</template>
<script>
import _l from "utils/localization";
import { mapGetters } from "vuex";
export default {
name: "FavoritesButton",
@@ -25,17 +26,21 @@ export default {
data() {
return {
searchKey: "#favorites",
tooltipToggle: _l("Show favorites"),
tooltipUntoggle: "Clear",
toggle: false,
};
},
computed: {
...mapGetters("user", ["currentUser"]),
tooltipText() {
if (this.toggle) {
return this.tooltipUntoggle;
if (this.currentUser.isAnonymous) {
return this.l("Log in to Favorite Tools");
} else {
return this.tooltipToggle;
if (this.toggle) {
return this.l("Clear");
} else {
return this.l("Show favorites");
}
}
},
},
+1 -1
View File
@@ -6,7 +6,7 @@
<h4 v-localize class="m-1">Tools</h4>
<div class="panel-header-buttons">
<b-button-group>
<favorites-button v-if="isUser" :query="query" @onFavorites="onQuery" />
<favorites-button :query="query" @onFavorites="onQuery" />
<panel-view-button
v-if="panelViews && Object.keys(panelViews).length > 1"
:panel-views="panelViews"
@@ -4,7 +4,9 @@
<slot name="search"> </slot>
</template>
<slot name="helper"> </slot>
<b-alert v-if="errorMessage" variant="danger" show v-html="errorMessage" />
<b-alert v-if="errorMessage" variant="danger" show>
{{ errorMessage }}
</b-alert>
<div v-else>
<slot v-if="optionsShow" name="options"> </slot>
<div v-else><span class="fa fa-spinner fa-spin" /> <span>Please wait...</span></div>
+3 -1
View File
@@ -3,7 +3,9 @@
<CurrentUser v-slot="{ user }">
<UserHistories v-if="user" v-slot="{ currentHistoryId }" :user="user">
<div v-if="currentHistoryId">
<b-alert :show="messageShow" :variant="messageVariant" v-html="messageText" />
<b-alert :show="messageShow" :variant="messageVariant">
{{ messageText }}
</b-alert>
<LoadingSpan v-if="showLoading" message="Loading Tool" />
<div v-if="showEntryPoints">
<ToolEntryPoints v-for="job in entryPoints" :key="job.id" :job-id="job.id" />
+1 -1
View File
@@ -66,7 +66,7 @@ export default {
return this.jobResponse && this.jobResponse.jobs ? this.jobResponse.jobs.length : 0;
},
nJobsText() {
return this.nJobs > 1 ? `${this.njobs} jobs` : `1 job`;
return this.nJobs > 1 ? `${this.nJobs} jobs` : `1 job`;
},
nInputsText() {
return this.nInputs > 1 ? `${this.nInputs} inputs` : `this input`;
+1 -1
View File
@@ -10,7 +10,7 @@
<div v-else-if="deprecated" class="warningmessagelarge">
You have used {{ getToolId }} tool. {{ deprecatedMessage }}
</div>
<div id="tool-recommendation" class="tool-recommendation-view"></div>
<div id="tool-recommendation" class="ui-tool-recommendation"></div>
</div>
</template>
@@ -1,4 +1,5 @@
import { createLocalVue, mount } from "@vue/test-utils";
import { mount } from "@vue/test-utils";
import { getLocalVue } from "jest/helpers";
import Monitor from "./Monitor";
jest.mock("app");
@@ -33,16 +34,19 @@ Services.mockImplementation(() => {
describe("Monitor", () => {
it("test monitor", async () => {
const localVue = createLocalVue();
const wrapper = mount(Monitor, localVue);
const localVue = getLocalVue();
const wrapper = mount(Monitor, { localVue });
await localVue.nextTick();
const headers = wrapper.findAll("th");
expect(headers.length).toBe(2);
expect(headers.length).toBe(3);
expect(headers.at(0).text()).toBe("Name");
expect(headers.at(1).text()).toBe("Status");
const cells = wrapper.findAll("td");
expect(cells.length).toBe(4);
expect(cells.length).toBe(6);
expect(cells.at(0).text()).toBe("name_0 (owner_0)");
expect(cells.at(2).text()).toBe("name_1 (owner_1)");
expect(cells.at(1).text()).toContain("status_0_0");
expect(cells.at(3).text()).toBe("name_1 (owner_1)");
expect(cells.at(4).text()).toContain("status_1");
});
});
@@ -15,7 +15,10 @@
<b-link @click="onQuery(row.item.name)"> {{ row.item.name }} ({{ row.item.owner }}) </b-link>
</template>
<template v-slot:cell(status)="row">
<InstallationButton
<b>Status: </b><span>{{ row.item.status }}</span>
</template>
<template v-slot:cell(actions)="row">
<InstallationActions
class="float-right"
:status="row.item.status"
@onUninstall="uninstallRepository(row.item)" />
@@ -29,13 +32,13 @@
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import { Services } from "../services";
import InstallationButton from "../RepositoryDetails/InstallationButton";
import InstallationActions from "../RepositoryDetails/InstallationActions";
Vue.use(BootstrapVue);
export default {
components: {
InstallationButton,
InstallationActions,
},
data() {
return {
@@ -43,7 +46,7 @@ export default {
loading: true,
error: null,
items: [],
fields: ["name", "status"],
fields: ["name", "status", "actions"],
};
},
computed: {
@@ -28,8 +28,17 @@
<span v-if="!data.value" :class="repoChecked" />
<span v-else :class="repoUnchecked" />
</template>
<template v-slot:cell(status)="row">
<span v-if="row.item.status">
<span
v-if="!['Error', 'Installed', 'Uninstalled'].includes(row.item.status)"
class="fa fa-spinner fa-spin" />
{{ row.item.status }}
</span>
<span v-else> - </span>
</template>
<template v-slot:cell(actions)="row">
<InstallationButton
<InstallationActions
:status="row.item.status"
@onInstall="setupRepository(row.item)"
@onUninstall="uninstallRepository(row.item)" />
@@ -61,7 +70,7 @@ import { Services } from "../services";
import ConfigProvider from "components/providers/ConfigProvider";
import ToolPanelViewProvider from "components/providers/ToolPanelViewProvider";
import InstallationSettings from "./InstallationSettings.vue";
import InstallationButton from "./InstallationButton.vue";
import InstallationActions from "./InstallationActions.vue";
import RepositoryTools from "./RepositoryTools.vue";
Vue.use(BootstrapVue);
@@ -71,7 +80,7 @@ export default {
ConfigProvider,
ToolPanelViewProvider,
InstallationSettings,
InstallationButton,
InstallationActions,
RepositoryTools,
},
props: {
@@ -96,7 +105,8 @@ export default {
{ key: "tools", label: "Tools and Versions" },
{ key: "profile", label: "Requires" },
{ key: "missing_test_components", label: "Tests" },
{ key: "actions", label: "" },
{ key: "status" },
{ key: "actions", label: "", class: "toolshed-repo-actions" },
],
showSettings: false,
error: null,
@@ -195,3 +205,11 @@ export default {
},
};
</script>
<style lang="scss">
// make actions take up less space
.toolshed-repo-actions {
width: 10%;
min-width: 120px;
}
</style>
@@ -0,0 +1,43 @@
<script setup>
import { computed } from "vue";
const props = defineProps({
status: String,
});
const installState = computed(() => !props.status || props.status === "Uninstalled");
const uninstallState = computed(() => props.status === "Installed");
const emit = defineEmits(["onInstall", "onUninstall"]);
function onCancel() {
if (window.confirm(`Do you want to reset this repository?`)) {
emit("onUninstall");
}
}
</script>
<template>
<div>
<b-button v-if="installState" variant="primary" class="btn-sm" @click="() => emit('onInstall')">
Install
</b-button>
<b-button v-else-if="uninstallState" variant="danger" class="btn-sm" @click="() => emit('onUninstall')">
Uninstall
</b-button>
<b-button
v-else
variant="warning"
class="btn-sm"
:title="l('Reset Broken or Stuck Installation')"
@click="onCancel">
Reset
</b-button>
</div>
</template>
<style lang="scss" scoped>
button {
min-width: 80px;
}
</style>
@@ -1,34 +0,0 @@
import { mount } from "@vue/test-utils";
import { getLocalVue } from "jest/helpers";
import InstallationButton from "./InstallationButton";
const localVue = getLocalVue();
describe("InstallationButton", () => {
it("test installed repository revision", () => {
const wrapper = mount(InstallationButton, {
propsData: {
installed: true,
status: "Installed",
},
localVue,
});
const $el = wrapper.find("button");
expect($el.classes()).toEqual(expect.arrayContaining(["btn-danger"]));
$el.trigger("click");
expect(wrapper.emitted().onUninstall).toBeDefined();
});
it("test uninstalled repository revision", () => {
const wrapper = mount(InstallationButton, {
propsData: {
installed: false,
status: "Uninstalled",
},
});
const $el = wrapper.find("button");
expect($el.classes()).toEqual(expect.arrayContaining(["btn-primary"]));
$el.trigger("click");
expect(wrapper.emitted().onInstall).toBeDefined();
});
});
@@ -1,58 +0,0 @@
<template>
<div>
<b-button v-if="installState" :class="buttonClass" variant="primary" @click="onInstall"> Install </b-button>
<div v-else>
<b-button v-if="uninstallState" :class="buttonClass" variant="danger" @click="onUninstall">
Uninstall
</b-button>
<b-button v-else :class="buttonClass" variant="info" @click="onCancel">
<span v-if="!errorState" class="fa fa-spinner fa-spin" />
<span>{{ status }}</span>
</b-button>
</div>
</div>
</template>
<script>
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
Vue.use(BootstrapVue);
export default {
props: {
status: {
type: String,
default: null,
},
},
data() {
return {
buttonClass: "btn-sm text-nowrap",
};
},
computed: {
installState() {
return !this.status || this.status == "Uninstalled";
},
uninstallState() {
return this.status == "Installed";
},
errorState() {
return this.status == "Error";
},
},
methods: {
onInstall() {
this.$emit("onInstall");
},
onUninstall() {
this.$emit("onUninstall");
},
onCancel() {
if (window.confirm(`Do you want to reset this repository?`)) {
this.$emit("onUninstall");
}
},
},
};
</script>
+7 -7
View File
@@ -3,30 +3,30 @@
<UserHistories v-if="user" v-slot="{ currentHistory, handlers, historiesLoading }" :user="user">
<div v-if="historiesLoading">computing tour requirements...</div>
<b-modal
v-else-if="loginRequired(user)"
id="tour-requirement-unment"
v-model="showRequirementDialog"
static
ok-only
hide-header
v-else-if="loginRequired(user)">
<b-alert show variant="danger"> You must login to Galaxy to use this tour. </b-alert>
hide-header>
<b-alert show variant="danger"> You must log in to Galaxy to use this tour. </b-alert>
</b-modal>
<b-modal
v-else-if="adminRequired(user)"
id="tour-requirement-unment"
v-model="showRequirementDialog"
static
ok-only
hide-header
v-else-if="adminRequired(user)">
hide-header>
<b-alert show variant="danger"> You must be an admin user to use this tour. </b-alert>
</b-modal>
<b-modal
v-else-if="newHistoryRequired(currentHistory, handlers)"
id="tour-requirement-unment"
v-model="showRequirementDialog"
static
ok-only
hide-header
v-else-if="newHistoryRequired(currentHistory, handlers)">
hide-header>
<b-alert show variant="danger">
This tour is designed to run on a new history, please create a new history before running it.
<a @click.prevent="handlers.createNewHistory()">Click here</a> to create a new history.
@@ -5,6 +5,8 @@ import MockProvider from "components/providers/MockProvider";
import flushPromises from "flush-promises";
import { getLocalVue } from "jest/helpers";
import DiskUsageSummary from "./DiskUsageSummary";
import { userStore } from "../../../store/userStore/userStore";
import Vuex from "vuex";
const localVue = getLocalVue();
@@ -20,6 +22,16 @@ const QuotaUsageProviderMock = MockProvider({
});
const QuotaUsageSummaryMock = { template: `<div id='${quotaUsageSummaryComponentId}'/>` };
const store = new Vuex.Store({
modules: {
user: {
state: fakeUser,
namespaced: true,
getters: userStore.getters,
},
},
});
async function mountDiskUsageSummaryWrapper(enableQuotas) {
const wrapper = mount(DiskUsageSummary, {
stubs: {
@@ -31,6 +43,7 @@ async function mountDiskUsageSummaryWrapper(enableQuotas) {
QuotaUsageSummary: QuotaUsageSummaryMock,
},
localVue,
store,
});
await flushPromises();
return wrapper;
@@ -8,10 +8,7 @@
</b-alert>
<b-container v-if="user">
<b-row v-if="config.enable_quotas" class="justify-content-md-center">
<QuotaUsageProvider v-slot="{ result: quotaUsages, loading: isLoadingUsage }">
<b-spinner v-if="isLoadingUsage" />
<QuotaUsageSummary v-else-if="quotaUsages" :quota-usages="quotaUsages" />
</QuotaUsageProvider>
<QuotaUsageSummary v-if="quotaUsages" :quota-usages="quotaUsages" />
</b-row>
<h2 v-else id="basic-disk-usage-summary" class="text-center my-3">
You're using <b>{{ getTotalDiskUsage(user) }}</b> of disk space.
@@ -50,17 +47,17 @@ import { bytesToString } from "utils/utils";
import CurrentUser from "components/providers/CurrentUser";
import ConfigProvider from "components/providers/ConfigProvider";
import QuotaUsageSummary from "components/User/DiskUsage/Quota/QuotaUsageSummary";
import { QuotaUsageProvider } from "./Quota/QuotaUsageProvider";
import { getAppRoot } from "onload/loadConfig";
import axios from "axios";
import { rethrowSimple } from "utils/simple-error";
import { QuotaUsage } from "./Quota/model";
import { mapGetters } from "vuex";
export default {
components: {
CurrentUser,
ConfigProvider,
QuotaUsageSummary,
QuotaUsageProvider,
},
data() {
return {
@@ -70,6 +67,12 @@ export default {
dismissCountDown: 0,
};
},
computed: {
...mapGetters("user", ["currentUser"]),
quotaUsages() {
return [new QuotaUsage(this.currentUser)];
},
},
methods: {
getTotalDiskUsage(user) {
return bytesToString(user.total_disk_usage, true);
@@ -79,7 +82,9 @@ export default {
},
async onRefresh() {
await this.requestDiskUsageRecalculation();
this.displayRecalculationForSeconds(30);
await this.displayRecalculationForSeconds(30);
this.$store.dispatch("user/loadUser");
},
async requestDiskUsageRecalculation() {
try {
@@ -88,12 +93,16 @@ export default {
rethrowSimple(e);
}
},
displayRecalculationForSeconds(seconds) {
this.isRecalculating = true;
this.dismissCountDown = seconds;
setTimeout(() => {
this.isRecalculating = false;
}, seconds * 1000);
async displayRecalculationForSeconds(seconds) {
return new Promise((resolve) => {
this.isRecalculating = true;
this.dismissCountDown = seconds;
setTimeout(() => {
this.isRecalculating = false;
resolve();
}, seconds * 1000);
});
},
countDownChanged(dismissCountDown) {
this.dismissCountDown = dismissCountDown;
@@ -1,24 +0,0 @@
import axios from "axios";
import { SingleQueryProvider } from "components/providers/SingleQueryProvider";
import { getAppRoot } from "onload/loadConfig";
import { rethrowSimple } from "utils/simple-error";
import { QuotaUsage } from "./model";
// TODO: replace this with the proper provider and API call after
// https://github.com/galaxyproject/galaxy/pull/10977 is available
/**
* Fetches the disk usage by the user across all ObjectStores.
* @returns {Array<QuotaUsage>}
*/
async function fetchQuotaUsage() {
const url = `${getAppRoot()}api/users/current`;
try {
const { data } = await axios.get(url);
return [new QuotaUsage(data)];
} catch (e) {
rethrowSimple(e);
}
}
export const QuotaUsageProvider = SingleQueryProvider(fetchQuotaUsage);
@@ -17,7 +17,7 @@
<p>
Users with existing Galaxy user accounts (e.g., via Galaxy username and password) can associate their
account with their 3rd party identities. For instance, if a user associates their Galaxy account with
their Google account, then they can login to Galaxy either using their Galaxy username and password, or
their Google account, then they can log in to Galaxy either using their Galaxy username and password, or
their Google account. Whichever method they use they will be assuming same Galaxy user account, hence
having access to the same histories, workflows, datasets, libraries, etc.
</p>
@@ -1,6 +1,8 @@
<template>
<div>
<b-alert :show="messageShow" :variant="messageVariant" v-html="messageText" />
<b-alert :show="messageShow" :variant="messageVariant">
{{ messageText }}
</b-alert>
<b-form id="externalLogin">
<!-- OIDC login-->
<hr class="my-4" />
@@ -9,11 +9,13 @@ export const getUserPreferencesModel = (user_id) => {
information: {
title: _l("Manage Information"),
id: "edit-preferences-information",
description: _l("Edit your email, addresses and custom parameters or change your public name."),
description: config.enable_account_interface
? _l("Edit your email, addresses and custom parameters or change your public name.")
: _l("Edit your custom parameters."),
url: `api/users/${user_id}/information/inputs`,
icon: "fa-user",
redirect: "user",
shouldRender: !config.use_remote_user && config.enable_account_interface,
shouldRender: !config.use_remote_user,
},
password: {
title: _l("Change Password"),
@@ -69,7 +71,7 @@ export const getUserPreferencesModel = (user_id) => {
icon: "fa-cloud",
submitTitle: "Create a new Key",
submitIcon: "fa-check",
shouldRender: config.enable_account_interface,
shouldRender: true,
},
toolbox_filters: {
title: _l("Manage Toolbox Filters"),
@@ -53,7 +53,7 @@ describe("DisplayApplications", () => {
it("check props", async () => {
const labels = wrapper.findAll(".font-weight-bold");
for (let i = 0; i < 2; i++) {
expect(labels.at(i).text()).toBe(`${i + 1}. app-${i + 1}`);
expect(labels.at(i).text()).toBe(`app-${i + 1}`);
}
const links = wrapper.findAll("a");
for (let i = 0; i < 3; i++) {
@@ -1,18 +1,35 @@
<template>
<div>
<DatasetProvider :id="datasetId" v-slot="{ result: dataset }">
<b-alert v-if="dataset && dataset.display_apps && dataset.display_apps.length > 0" variant="info" show>
<b-alert
v-if="
(dataset && dataset.display_apps && dataset.display_apps.length > 0) ||
(dataset && dataset.display_types && dataset.display_types.length > 0)
"
variant="info"
show>
You can display your dataset with the following links:
<div class="p-2">
<div v-for="(displayApp, displayKey) in dataset.display_apps" :key="displayKey">
<span class="font-weight-bold">{{ displayKey + 1 }}. {{ displayApp.label }}</span>
<span v-for="(link, linkKey) in displayApp.links" :key="linkKey">
<span v-if="linkKey == 0">(</span>
<b-link :href="link.href" :target="link.target">{{ link.text }}</b-link>
<span v-if="linkKey != displayApp.links.length - 1">, </span>
<span v-else>)</span>
</span>
</div>
<ol>
<li v-for="(displayApp, displayKey) in dataset.display_apps" :key="displayKey">
<span class="font-weight-bold">{{ displayApp.label }}</span>
<span v-for="(link, linkKey) in displayApp.links" :key="linkKey">
<span v-if="linkKey == 0">(</span>
<b-link :href="link.href" :target="link.target">{{ link.text }}</b-link>
<span v-if="linkKey != displayApp.links.length - 1">, </span>
<span v-else>)</span>
</span>
</li>
<li v-for="(displayType, displayKey) in dataset.display_types" :key="displayKey">
<span class="font-weight-bold">{{ displayType.label }}</span>
<span v-for="(link, linkKey) in displayType.links" :key="linkKey">
<span v-if="linkKey == 0">(</span>
<b-link :href="link.href" :target="link.target">{{ link.text }}</b-link>
<span v-if="linkKey != displayType.links.length - 1">, </span>
<span v-else>)</span>
</span>
</li>
</ol>
</div>
<div>or select a visualization from below.</div>
</b-alert>
@@ -16,7 +16,7 @@ jest.mock("app");
import { getDatatypesMapper } from "components/Datatypes/factory";
import { testDatatypesMapper } from "components/Datatypes/test_fixtures";
import { getVersions, loadWorkflow } from "./modules/services";
import { getStateUpgradeMessages, saveAs } from "./modules/utilities";
import { getStateUpgradeMessages } from "./modules/utilities";
import { getAppRoot } from "onload/loadConfig";
import WorkflowCanvas from "./modules/canvas";
@@ -106,13 +106,6 @@ describe("Index", () => {
expect(wrapper.vm.hasChanges).toBeTruthy();
});
it("delegates to a module onSaveAs", async () => {
mountAndWaitForCreated();
saveAs.mockReturnThis();
wrapper.vm.onSaveAs();
expect(saveAs).toBeCalled();
});
it("prevents navigation only if hasChanges", async () => {
mountAndWaitForCreated();
expect(wrapper.vm.hasChanges).toBeFalsy();
@@ -13,6 +13,19 @@
@onRefactor="onRefactor"
@onShow="hideModal" />
<MessagesModal :title="messageTitle" :message="messageBody" :error="messageIsError" @onHidden="resetMessage" />
<b-modal
v-model="showSaveAsModal"
title="Save As a New Workflow"
ok-title="Save"
cancel-title="Cancel"
@ok="doSaveAs">
<b-form-group label="Name">
<b-form-input v-model="saveAsName" />
</b-form-group>
<b-form-group label="Annotation">
<b-form-textarea v-model="saveAsAnnotation" />
</b-form-group>
</b-modal>
<MarkdownEditor
v-if="!isCanvas"
:markdown-text="markdownText"
@@ -175,12 +188,13 @@
</template>
<script>
import axios from "axios";
import { LastQueue } from "utils/promise-queue";
import { getDatatypesMapper } from "components/Datatypes";
import { fromSimple } from "./modules/model";
import { fromSimple, toSimple } from "./modules/model";
import { getModule, getVersions, saveWorkflow, loadWorkflow } from "./modules/services";
import { getUntypedWorkflowParameters } from "./modules/parameters";
import { getStateUpgradeMessages, copyIntoWorkflow, saveAs } from "./modules/utilities";
import { getStateUpgradeMessages } from "./modules/utilities";
import WorkflowCanvas from "./modules/canvas";
import WorkflowOptions from "./Options";
import FormDefault from "./Forms/FormDefault";
@@ -273,6 +287,9 @@ export default {
showInPanel: "attributes",
isWheeled: false,
canvasManager: null,
saveAsName: null,
saveAsAnnotation: null,
showSaveAsModal: false,
};
},
computed: {
@@ -452,18 +469,58 @@ export default {
onInsertWorkflow(workflow_id, workflow_name) {
this._insertStep(workflow_id, workflow_name, "subworkflow");
},
onInsertWorkflowSteps(workflow_id, step_count) {
copyIntoWorkflow(id = null) {
// Load workflow definition
this.onWorkflowMessage("Importing workflow", "progress");
loadWorkflow({ workflow: this, id, appendData: true }).then((data) => {
// Determine if any parameters were 'upgraded' and provide message
const insertedStateMessages = getStateUpgradeMessages(data);
this.onInsertedStateMessages(insertedStateMessages);
});
},
async onInsertWorkflowSteps(workflowId, stepCount) {
if (!this.isCanvas) {
this.isCanvas = true;
return;
}
copyIntoWorkflow(this, workflow_id, step_count);
if (stepCount < 10) {
this.copyIntoWorkflow(workflowId);
} else {
const confirmed = await this.$bvModal.msgBoxConfirm(
`Warning this will add ${stepCount} new steps into your current workflow. You may want to consider using a subworkflow instead.`
);
if (confirmed) {
this.copyIntoWorkflow(workflowId);
}
}
},
onDownload() {
window.location = `${getAppRoot()}api/workflows/${this.id}/download?format=json-download`;
},
doSaveAs() {
const rename_name = this.saveAsName ?? `SavedAs_${this.name}`;
const rename_annotation = this.saveAsAnnotation ?? "";
// This is an old web controller endpoint that wants form data posted...
const formData = new FormData();
formData.append("workflow_name", rename_name);
formData.append("workflow_annotation", rename_annotation);
formData.append("from_tool_form", true);
formData.append("workflow_data", JSON.stringify(toSimple(this)));
axios
.post(`${getAppRoot()}workflow/save_workflow_as`, formData)
.then((response) => {
this.onWorkflowMessage("Workflow saved as", "success");
this.hideModal();
this.onNavigate(`${getAppRoot()}workflow/editor?id=${response.data}`, true);
})
.catch((response) => {
this.onWorkflowError("Saving workflow failed, please contact an administrator.");
});
},
onSaveAs() {
saveAs(this);
this.showSaveAsModal = true;
},
onLayout() {
this.canvasManager.drawOverview();
@@ -70,6 +70,7 @@
v-for="output in outputs"
:key="output.name"
:output="output"
:post-job-actions="postJobActions"
:get-node="getNode"
:get-manager="getManager"
@onAdd="onAddOutput"
@@ -61,10 +61,11 @@ export default {
oldTerminal.destroyInvalidConnections();
} else {
// create new terminal, connect like old terminal, destroy old terminal
// this might be a little buggy, the terminals and connectors should be vue components
this.$emit("onRemove", this.input);
const newTerminal = this.createTerminal(newInput);
newTerminal.connectors = this.terminal.connectors.map((c) => {
return new Connector(this.getManager(), c.outputHandle, newTerminal);
return new Connector(this.getManager().canvasManager, c.outputHandle, newTerminal);
});
newTerminal.destroyInvalidConnections();
this.terminal = newTerminal;
@@ -34,6 +34,10 @@ export default {
type: Function,
required: true,
},
postJobActions: {
type: Object,
required: true,
},
},
data() {
return {
@@ -46,12 +50,8 @@ export default {
return `node-${node.id}-output-${this.output.name}`;
},
label() {
let extensions = this.output.extensions || this.output.type || "unspecified";
if (Array.isArray(extensions)) {
extensions = extensions.join(", ");
}
const activeLabel = this.output.activeLabel || this.output.label || this.output.name;
return `${activeLabel} (${extensions})`;
return `${activeLabel} (${this.extensions.join(", ")})`;
},
activeClass() {
return this.output.activeOutput && "mark-terminal-active";
@@ -67,6 +67,19 @@ export default {
}
return cls;
},
forcedExtension() {
const changeDatatype =
this.postJobActions[`ChangeDatatypeAction${this.output.label}`] ||
this.postJobActions[`ChangeDatatypeAction${this.output.name}`];
return changeDatatype?.action_arguments.newtype;
},
extensions() {
let extensions = this.forcedExtension || this.output.extensions || this.output.type || "unspecified";
if (!Array.isArray(extensions)) {
extensions = [extensions];
}
return extensions;
},
},
watch: {
label() {
@@ -78,14 +91,15 @@ export default {
output(newOutput) {
const oldTerminal = this.terminal;
if (oldTerminal instanceof this.terminalClassForOutput(newOutput)) {
oldTerminal.update(newOutput);
oldTerminal.update({ ...newOutput, extensions: this.extensions });
oldTerminal.destroyInvalidConnections();
} else {
// create new terminal, connect like old terminal, destroy old terminal
// this might be a little buggy, we should replace this with proper vue components
this.$emit("onRemove", this.output);
this.createTerminal(newOutput);
this.terminal.connectors = oldTerminal.connectors.map((c) => {
return new Connector(this.getManager(), this.terminal, c.inputHandle);
return new Connector(this.getManager().canvasManager, this.terminal, c.inputHandle);
});
this.terminal.destroyInvalidConnections();
oldTerminal.destroy();
@@ -124,7 +138,7 @@ export default {
...parameters,
collection_type: collection_type,
collection_type_source: collection_type_source,
datatypes: output.extensions,
datatypes: this.extensions,
});
} else if (output.parameter) {
this.terminal = new terminalClass({
@@ -134,7 +148,7 @@ export default {
} else {
this.terminal = new terminalClass({
...parameters,
datatypes: output.extensions,
datatypes: this.extensions,
});
}
new OutputDragging(this.getManager(), {
@@ -1,4 +1,3 @@
import $ from "jquery";
import * as d3 from "d3";
import { Toast } from "ui/toast";
@@ -13,9 +12,17 @@ const ribbonOuterSingle = 6;
const ribbonInnerMultiple = 1;
const ribbonOuterMultiple = 3;
function offset(element) {
const rect = element.getBoundingClientRect();
return {
top: rect.top + window.scrollY,
left: rect.left + window.scrollX,
};
}
class Connector {
constructor(manager = {}, outputHandle = null, inputHandle = null) {
this.manager = manager;
constructor(canvasManager = {}, outputHandle = null, inputHandle = null) {
this.manager = canvasManager;
this.dragging = false;
this.innerClass = "ribbon-inner";
this.canvas = document.createElement("div");
@@ -59,31 +66,33 @@ class Connector {
}
redraw() {
// Identify handles
const canvasContainer = $("#canvas-container");
const canvasContainer = document.getElementById("canvas-container");
const outputHandle = this.outputHandle;
const inputHandle = this.inputHandle;
if (!canvasContainer.length || !outputHandle || !inputHandle) {
if (!canvasContainer || !outputHandle || !inputHandle) {
return;
}
const canvasZoom = this.manager.canvasZoom;
if (this.dragging) {
this.canvas.style.zIndex = zIndex;
}
const relativeLeft = (e) => ($(e).offset().left - canvasContainer.offset().left) / canvasZoom;
const relativeTop = (e) => ($(e).offset().top - canvasContainer.offset().top) / canvasZoom;
if (!outputHandle || !inputHandle) {
return;
}
// Set handle ids, used in test cases
this.canvas.setAttribute("output-handle-id", outputHandle.element.getAttribute("id"));
this.canvas.setAttribute("input-handle-id", inputHandle.element.getAttribute("id"));
// Find the position of each handle
let start_x = relativeLeft(outputHandle.element) + handleMarginX;
let start_y = relativeTop(outputHandle.element) + 0.5 * $(outputHandle.element).height();
let end_x = relativeLeft(inputHandle.element) + handleMarginX;
let end_y = relativeTop(inputHandle.element) + 0.5 * $(inputHandle.element).height();
const canvasZoom = this.manager.canvasZoom;
const relativeLeft = (e) => (offset(e).left - offset(canvasContainer).left) / canvasZoom + handleMarginX;
const relativeTop = (e) =>
(offset(e).top - offset(canvasContainer).top) / canvasZoom + 0.5 * inputHandle.element.offsetHeight;
let start_x = relativeLeft(outputHandle.element);
let start_y = relativeTop(outputHandle.element);
let end_x = relativeLeft(inputHandle.element);
let end_y = relativeTop(inputHandle.element);
// Calculate canvas area
const canvas_min_x = Math.min(start_x, end_x);
@@ -333,34 +333,37 @@ class BaseInputTerminal extends Terminal {
}
_producesAcceptableDatatype(other) {
// other is a non-collection output...
for (var t in this.datatypes) {
var thisDatatype = this.datatypes[t];
for (const t in this.datatypes) {
const thisDatatype = this.datatypes[t];
if (thisDatatype == "input") {
return new ConnectionAcceptable(true, null);
}
var cat_outputs = [];
const other_datatypes = other.force_datatype ? [other.force_datatype] : other.datatypes;
cat_outputs = cat_outputs.concat(other_datatypes);
// FIXME: No idea what to do about case when datatype is 'input'
for (var other_datatype_i in cat_outputs) {
var other_datatype = cat_outputs[other_datatype_i];
if (
const validMatch = other.datatypes.some(
(other_datatype) =>
other_datatype == "input" ||
other_datatype == "_sniff_" ||
this._isSubType(cat_outputs[other_datatype_i], thisDatatype)
) {
return new ConnectionAcceptable(true, null);
} else if (!this.datatypesMapper.datatypes.includes(other_datatype)) {
return new ConnectionAcceptable(
false,
`Effective output data type [${other_datatype}] unknown. This tool cannot be executed on this Galaxy Server at this moment, please contact the Administrator.`
);
}
this._isSubType(other_datatype, thisDatatype)
);
if (validMatch) {
return new ConnectionAcceptable(true, null);
}
}
const datatypesSet = new Set(this.datatypesMapper.datatypes);
const invalidDatatypes = other.datatypes.filter((datatype) => !datatypesSet.has(datatype));
if (invalidDatatypes.length) {
return new ConnectionAcceptable(
false,
`Effective output data type(s) [${invalidDatatypes.join(
", "
)}] unknown. This tool cannot be executed on this Galaxy Server at this moment, please contact the Administrator.`
);
}
return new ConnectionAcceptable(
false,
`Effective output data type(s) [${cat_outputs}] do not appear to match input type(s) [${this.datatypes}].`
`Effective output data type(s) [${other.datatypes.join(
", "
)}] do not appear to match input type(s) [${this.datatypes.join(", ")}].`
);
}
_isSubType(child, parent) {
@@ -606,10 +609,6 @@ class BaseOutputTerminal extends Terminal {
this.setMapOver(this.node.mapOver);
}
}
get force_datatype() {
const changeOutputDatatype = this.node.postJobActions["ChangeDatatypeAction" + this.name];
return changeOutputDatatype ? changeOutputDatatype.action_arguments["newtype"] : null;
}
update(output) {
this.datatypes = output.datatypes || output.extensions;
this.optional = output.optional;
@@ -1,35 +1,6 @@
import _ from "underscore";
import $ from "jquery";
import { getAppRoot } from "onload/loadConfig";
import _l from "utils/localization";
import { loadWorkflow } from "./services";
import { toSimple } from "./model";
import { show_modal } from "layout/modal";
import WorkflowIcons from "components/Workflow/icons";
export function copyIntoWorkflow(workflow, id = null, stepCount = null) {
const _copy_into_workflow_ajax = () => {
// Load workflow definition
workflow.onWorkflowMessage("Importing workflow", "progress");
loadWorkflow({ workflow, id, appendData: true }).then((data) => {
// Determine if any parameters were 'upgraded' and provide message
const insertedStateMessages = getStateUpgradeMessages(data);
workflow.onInsertedStateMessages(insertedStateMessages);
});
};
if (stepCount < 2) {
_copy_into_workflow_ajax();
} else {
// don't ruin the workflow by adding 50 steps unprompted.
show_modal(_l("Warning"), `This will copy ${stepCount} new steps into your workflow.`, {
Cancel: () => {
workflow.hideModal();
},
Copy: _copy_into_workflow_ajax,
});
}
}
export function getStateUpgradeMessages(data) {
// Determine if any parameters were 'upgraded' and provide message
const messages = [];
@@ -56,45 +27,6 @@ export function getStateUpgradeMessages(data) {
return messages;
}
export function saveAs(workflow) {
var body = $(
'<form><label style="display:inline-block; width: 100%;">Save as name: </label><input type="text" id="workflow_rename" style="width: 80%;" autofocus/>' +
'<br><label style="display:inline-block; width: 100%;">Annotation: </label><input type="text" id="wf_annotation" style="width: 80%;" /></form>'
);
show_modal("Save As a New Workflow", body, {
OK: function () {
var rename_name =
$("#workflow_rename").val().length > 0 ? $("#workflow_rename").val() : `SavedAs_${workflow.name}`;
var rename_annotation = $("#wf_annotation").val().length > 0 ? $("#wf_annotation").val() : "";
$.ajax({
url: `${getAppRoot()}workflow/save_workflow_as`,
type: "POST",
data: {
workflow_name: rename_name,
workflow_annotation: rename_annotation,
from_tool_form: true,
workflow_data: () => {
return JSON.stringify(toSimple(workflow));
},
},
})
.done((id) => {
workflow.onNavigate(`${getAppRoot()}workflows/edit?id=${id}`, true);
})
.fail((err) => {
console.debug(err);
workflow.onWorkflowError(
"Saving this workflow failed. Please contact this site's administrator.",
err
);
});
},
Cancel: () => {
workflow.hideModal();
},
});
}
export function getCompatibleRecommendations(predChild, outputDatatypes, datatypesMapper) {
const cTools = [];
const toolMap = new Map();
@@ -96,6 +96,9 @@ describe("Invocations.vue", () => {
propsData,
computed: {
getWorkflowNameByInstanceId: (state) => (id) => "workflow name",
getStoredWorkflowIdByInstanceId: (state) => (id) => {
return "workflowId";
},
getWorkflowByInstanceId: (state) => (id) => {
return { id: "workflowId" };
},
+10 -7
View File
@@ -65,7 +65,10 @@
<UtcDate :date="data.value" mode="elapsed" />
</template>
<template v-slot:cell(execute)="data">
<WorkflowRunButton :id="getWorkflowByInstanceId(data.item.workflow_id).id" :root="root" />
<WorkflowRunButton
v-if="getStoredWorkflowIdByInstanceId(data.item.workflow_id)"
:id="getStoredWorkflowIdByInstanceId(data.item.workflow_id)"
:root="root" />
</template>
</b-table>
<b-pagination
@@ -122,7 +125,7 @@ export default {
};
},
computed: {
...mapGetters(["getWorkflowNameByInstanceId", "getWorkflowByInstanceId"]),
...mapGetters(["getWorkflowNameByInstanceId", "getWorkflowByInstanceId", "getStoredWorkflowIdByInstanceId"]),
...mapGetters("history", ["getHistoryById", "getHistoryNameById"]),
title() {
let title = `Workflow Invocations`;
@@ -187,19 +190,19 @@ export default {
.invocations-table {
min-width: 40rem;
}
.table::v-deep .col-name {
.table:deep(.col-name) {
width: 40%;
}
.table::v-deep .col-history {
.table:deep(.col-history) {
width: 20%;
}
.table::v-deep .col-small {
.table:deep(.col-small) {
width: 100px;
}
.table::v-deep .col-button {
.table:deep(.col-button) {
width: 50px;
}
.table::v-deep .truncate {
.table:deep(.truncate) {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
@@ -29,7 +29,7 @@ import { mapGetters } from "vuex";
import { WorkflowInvocationState } from "components/WorkflowInvocationState";
import Webhooks from "mvc/webhooks";
import { getAppRoot } from "onload/loadConfig";
import { getGalaxyInstance } from "app";
import { refreshContentsWrapper } from "utils/data";
export default {
components: {
@@ -45,11 +45,6 @@ export default {
required: true,
},
},
data() {
return {
refreshHistoryTimeout: null,
};
},
computed: {
...mapGetters("history", ["currentHistoryId"]),
timesExecuted() {
@@ -78,26 +73,7 @@ export default {
toolId: null,
toolVersion: null,
});
this._refreshHistory();
},
methods: {
_refreshHistory() {
// remove when disabling backbone history
const Galaxy = getGalaxyInstance();
var history = Galaxy && Galaxy.currHistoryPanel && Galaxy.currHistoryPanel.model;
if (this.refreshHistoryTimeout) {
window.clearTimeout(this.refreshHistoryTimeout);
}
if (history && history.refresh) {
history.refresh().success(() => {
if (history.numOfUnfinishedShownContents() === 0) {
this.refreshHistoryTimeout = window.setTimeout(() => {
this._refreshHistory();
}, history.UPDATE_DELAY);
}
});
}
},
refreshContentsWrapper();
},
};
</script>
@@ -1,5 +1,5 @@
<template>
<b-link v-b-tooltip.hover :title="title" @click="onClick">
<b-link v-b-tooltip.hover :title="title" class="workflow-bookmark-link" @click="onClick">
<font-awesome-icon v-if="checked" :icon="['fas', 'star']" />
<font-awesome-icon v-else :icon="['far', 'star']" />
</b-link>
@@ -0,0 +1,160 @@
<template>
<div>
<p>
A BioCompute Object (BCO) is the unofficial name for a JSON object that adheres to the
<a href="https://standards.ieee.org/ieee/2791/7337/">IEEE-2791-2020 standard</a>. A BCO is designed to
communicate High-throughput Sequencing (HTS) analysis results, data set creation, data curation, and
bioinformatics verification protocols.
</p>
<p>Learn more about <a href="https://biocomputeobject.org/" target="_blank">BioCompute Objects</a>.</p>
<p>
Instructions for
<a href="https://w3id.org/biocompute/tutorials/galaxy_quick_start" target="_blank"
>creating a BCO using Galaxy</a
>.
</p>
<b-tabs lazy>
<b-tab title="Download">
<a class="bco-json" style="padding-left: 1em" :href="bcoDownloadLink"><b>Download BCO</b></a>
</b-tab>
<b-tab title="Submit To BCODB">
<div>
<p>
To submit to a BCODB you need to already have an authenticated account. Instructions on
submitting a BCO from Galaxy are available
<a href="https://w3id.org/biocompute/tutorials/galaxy_quick_start/" target="_blank">here</a>.
</p>
<form @submit.prevent="submitForm">
<div class="form-group">
<label for="fetch">BCO DB URL (example: https://biocomputeobject.org)</label>
<input
id="fetch"
v-model="form.fetch"
type="text"
class="form-control"
placeholder="https://biocomputeobject.org"
required />
</div>
<div class="form-group">
<label for="authorization">User API Key</label>
<input
id="authorization"
v-model="form.authorization"
type="password"
class="form-control"
required />
</div>
<div class="form-group">
<label for="table">Prefix</label>
<input
id="table"
v-model="form.table"
type="text"
class="form-control"
placeholder="BCO"
required />
</div>
<div class="form-group">
<label for="owner_group">User Name</label>
<input
id="owner_group"
v-model="form.owner_group"
type="text"
class="form-control"
required />
</div>
<div class="form-group">
<button class="btn btn-primary">{{ "Submit" | localize }}</button>
</div>
</form>
</div>
</b-tab>
</b-tabs>
</div>
</template>
<script>
import { getRootFromIndexLink } from "onload";
import { getAppRoot } from "onload/loadConfig";
import axios from "axios";
const getUrl = (path) => getRootFromIndexLink() + path;
export default {
props: {
invocationId: {
type: String,
required: true,
},
},
data() {
return {
form: {
fetch: "",
authorization: "",
table: "",
owner_group: "",
},
};
},
computed: {
bcoDownloadLink: function () {
return `${getAppRoot()}api/invocations/${this.invocationId}/biocompute/download`;
},
},
methods: {
async submitForm() {
const bco = await axios
.get(getUrl(`./api/invocations/${this.invocationId}/biocompute/`))
.then((response) => {
this.bco = response.data;
return this.bco;
})
.catch((e) => {
this.errors.push(e);
});
const bcoString = {
POST_api_objects_draft_create: [
{
contents: bco,
owner_group: this.form.owner_group,
schema: "IEEE",
prefix: this.form.table,
},
],
};
const headers = {
Authorization: "Token " + this.form.authorization,
"Content-type": "application/json; charset=UTF-8",
};
const submitURL = this.form.fetch;
axios
.post(`${submitURL}/api/objects/drafts/create/`, bcoString, { headers: headers })
.then((response) => {
if (response.status === 200) {
console.log("response:", response);
alert(response.data[0].message);
}
if (response.status === 207) {
console.log("response:", response);
alert(response.data[0].message);
}
})
.catch(function (error) {
console.log("Error", { ...error });
if (error.response.status == 401) {
alert(error.response.data.detail);
console.log("Error response: ", error);
} else {
console.log("Error", error);
alert("Error", error);
}
});
this.form.owner_group = "";
this.form.authorization = "";
this.form.owner_group = "";
this.form.fetch = "";
},
},
};
</script>
@@ -0,0 +1 @@
export { default as BioComputeObjectExportCard } from "./BioComputeObjectExportCard";
@@ -1,32 +1,21 @@
<template>
<b-card-group deck>
<b-card title="BioCompute Object" class="export-plugin-card">
<p>
A BioCompute Object (BCO) is the unofficial name for a JSON object that adheres to the
<a href="https://standards.ieee.org/ieee/2791/7337/">IEEE-2791-2020 standard</a>. A BCO is designed to
communicate High-throughput Sequencing (HTS) analysis results, data set creation, data curation, and
bioinformatics verification protocols.
</p>
<p>Learn more about <a href="https://biocomputeobject.org/" target="_blank">BioCompute Objects</a>.</p>
<a class="bco-json" :href="bcoDownloadLink"><b>Download</b></a>
<b-card title="BioCompute Object Export" class="export-plugin-card">
<bio-compute-object-export-card :invocation-id="invocationId" />
</b-card>
<!-- Add more export plugins here -->
</b-card-group>
</template>
<script>
import { getAppRoot } from "onload/loadConfig";
import BioComputeObjectExportCard from "./Export/BioComputeObjectExportCard.vue";
export default {
components: { BioComputeObjectExportCard },
props: {
invocationId: {
type: String,
required: true,
},
},
computed: {
bcoDownloadLink: function () {
return `${getAppRoot()}api/invocations/${this.invocationId}/biocompute/download`;
},
},
};
</script>
+155
View File
@@ -0,0 +1,155 @@
<template>
<div class="unified-panel">
<div class="unified-panel-header" unselectable="on">
<div class="unified-panel-header-inner">
<div class="panel-header-text">Galaxy Version {{ versionMajor }}</div>
</div>
</div>
<div class="unified-panel-body">
<div class="toolMenuContainer">
<div class="toolSectionWrapper">
<div
v-for="(section, sectionIndex) in sections"
:key="sectionIndex"
class="toolSectionTitle pt-1 px-3">
{{ section.title }}
<div class="toolSectionBody">
<div v-for="(item, itemIndex) in section.items" :key="itemIndex" class="toolTitle">
<router-link v-if="!item.disabled" class="title-link" :id="item.id" :to="item.route">
<small class="name">{{ item.title }}</small>
</router-link>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
enableQuotas: {
type: Boolean,
default: false,
},
isToolshedInstalled: {
type: Boolean,
default: false,
},
versionMajor: {
type: String,
default: "",
},
},
data() {
return {
sections: [
{
title: "Server",
items: [
{
id: "admin-link-datatypes",
title: "Data Types",
route: "/admin/data_types",
},
{
id: "admin-link-data-tables",
title: "Data Tables",
route: "/admin/data_tables",
},
{
id: "admin-link-display-applications",
title: "Display Applications",
route: "/admin/display_applications",
},
{
id: "admin-link-jobs",
title: "Jobs",
route: "/admin/jobs",
},
{
id: "admin-link-invocations",
title: "Workflow Invocations",
route: "/admin/invocations",
},
{
id: "admin-link-local-data",
title: "Local Data",
route: "/admin/data_manager",
},
],
},
{
title: "User Management",
items: [
{
id: "admin-link-users",
title: "Users",
route: "/admin/users",
},
{
id: "admin-link-quotas",
title: "Quotas",
route: "/admin/quotas",
disabled: !this.enableQuotas,
},
{
id: "admin-link-groups",
title: "Groups",
route: "/admin/groups",
},
{
id: "admin-link-roles",
title: "Roles",
route: "/admin/roles",
},
{
id: "admin-link-forms",
title: "Forms",
route: "/admin/forms",
},
],
},
{
title: "Tool Management",
items: [
{
id: "admin-link-toolshed",
title: "Install and Uninstall",
route: "/admin/toolshed",
disabled: !this.isToolshedInstalled,
},
{
id: "admin-link-metadata",
title: "Manage Metadata",
route: "/admin/reset_metadata",
},
{
id: "admin-link-allowlist",
title: "Manage Allowlist",
route: "/admin/sanitize_allow",
},
{
id: "admin-link-manage-dependencies",
title: "Manage Dependencies",
route: "/admin/toolbox_dependencies",
},
{
id: "admin-link-tool-versions",
title: "View Lineage",
route: "/admin/tool_versions",
},
{
id: "admin-link-error-stack",
title: "View Error Logs",
route: "/admin/error_stack",
},
],
},
],
};
},
};
</script>
@@ -116,7 +116,7 @@ export default {
return [
{
text: "Data Managers",
to: "/",
to: { name: "DataManager" },
},
{
text: this.dataManager["name"] + " ( " + this.dataManager["description"] + " )",
@@ -1,43 +0,0 @@
import { getAppRoot } from "onload/loadConfig";
import Vue from "vue";
import VueRouter from "vue-router";
import DataManager from "./DataManager.vue";
import DataManagerJobs from "./DataManagerJobs.vue";
import DataManagerJob from "./DataManagerJob.vue";
import DataManagerTable from "./DataManagerTable.vue";
Vue.use(VueRouter);
export default new VueRouter({
mode: "history",
base: `${getAppRoot()}admin/data_manager`,
routes: [
{
path: "/jobs/:id",
name: "DataManagerJobs",
component: DataManagerJobs,
props: true,
},
{
path: "/job/:id",
name: "DataManagerJob",
component: DataManagerJob,
props: true,
},
{
path: "/table/:name",
name: "DataManagerTable",
component: DataManagerTable,
props: true,
},
{
path: "/",
name: "DataManager",
component: DataManager,
},
{
path: "*",
redirect: { name: "DataManager" },
},
],
});
@@ -74,7 +74,7 @@ export default {
return [
{
text: "Tool Data Tables",
to: "/",
to: "/admin/data_manager",
},
{
text: this.dataTableName,
@@ -1,14 +0,0 @@
import router from "./DataManagerRouter";
import DataManagerView from "./DataManagerView";
export const DataManager = {
router,
render: (h) => h(DataManagerView),
created() {
if (router.currentRoute.name !== "DataManager") {
router.push({ name: "DataManager" });
}
},
};
export default DataManager;
+18 -49
View File
@@ -10,32 +10,32 @@
<ul>
<li>
<strong>
<a :href="adminDataTypesUrl" @click.prevent="useRouter">Data Types</a>
<router-link to="/admin/data_types">Data Types</router-link>
</strong>
- See all datatypes available in this Galaxy.
</li>
<li>
<strong>
<a :href="adminDataTablesUrl" @click.prevent="useRouter">Data Tables</a>
<router-link to="/admin/data_tables">Data Tables</router-link>
</strong>
- See all data tables available in this Galaxy.
</li>
<li>
<strong>
<a :href="adminDisplayApplicationsUrl" @click.prevent="useRouter">Display Applications</a>
<router-link to="/admin/display_applications">Display Applications</router-link>
</strong>
- See all display applications configured in this Galaxy.
</li>
<li>
<strong>
<a :href="adminJobsUrl" @click.prevent="useRouter">Jobs</a>
<router-link to="/admin/jobs">Jobs</router-link>
</strong>
- Display all jobs that are currently not finished (i.e., their state is new, waiting, queued, or
running). Administrators are able to cleanly stop long-running jobs.
</li>
<li>
<strong>
<a :href="adminDMUrl" @click.prevent="useRouter">Local Data</a>
<router-link to="/admin/data_manager">Local Data</router-link>
</strong>
- Manage the reference (and other) data that is stored within Tool Data Tables. See
<a href="https://galaxyproject.org/admin/tools/data-managers" target="_blank">wiki</a> for details.
@@ -46,7 +46,7 @@
<ul>
<li>
<strong>
<a :href="adminUsersUrl" @click.prevent="useRouter">Users</a>
<router-link to="/admin/users">Users</router-link>
</strong>
- The primary user management interface, displaying information associated with each user and providing
operations for resetting passwords, updating user information, impersonating a user, and more.
@@ -54,7 +54,7 @@
<!-- %if trans.app.config.enable_quotas: -->
<li>
<strong>
<a :href="adminQuotasUrl" @click.prevent="useRouter">Quotas</a>
<router-link to="/admin/quotas">Quotas</router-link>
</strong>
- Manage user space quotas. See
<a href="https://galaxyproject.org/admin/disk-quotas" target="_blank">wiki</a> for details.
@@ -62,13 +62,13 @@
<!-- %endif -->
<li>
<strong>
<a :href="adminGroupsUrl" @click.prevent="useRouter">Groups</a>
<router-link to="/admin/groups">Groups</router-link>
</strong>
- A view of all groups along with the members of the group and the roles associated with each group.
</li>
<li>
<strong>
<a :href="adminRolesUrl" @click.prevent="useRouter">Roles</a>
<router-link to="/admin/roles">Roles</router-link>
</strong>
- A view of all non-private roles along with the role type, and the users and groups that are
associated, with the role. Also includes a view of the data library datasets that are associated with
@@ -76,7 +76,7 @@
</li>
<li>
<strong>
<a :href="adminFormsUrl" @click.prevent="useRouter">Forms</a>
<router-link to="/admin/forms">Forms</router-link>
</strong>
- Manage local form definitions.
</li>
@@ -86,28 +86,28 @@
<ul>
<li>
<strong>
<a :href="adminSanitizeAllowUrl" @click.prevent="useRouter">Manage Allowlist</a>
<router-link to="/admin/sanitize_allow">Manage Allowlist</router-link>
</strong>
- Manage HTML rendering for installed tools' output datasets.
</li>
<li v-if="isToolShedInstalled">
<li v-if="isToolshedInstalled">
<strong>
<a :href="adminToolshedUrl" @click.prevent="useRouter">Install and Uninstall</a>
<router-link to="/admin/toolshed">Install and Uninstall</router-link>
</strong>
- Search and install new tools and other Galaxy utilities from the Tool Shed. See
<a href="https://galaxyproject.org/admin/tools/add-tool-from-toolshed-tutorial" target="_blank"
>the tutorial</a
>.
</li>
<li v-if="isRepoInstalled">
<li>
<strong>
<a :href="adminManageMetadata" @click.prevent="useRouter">Manage Metadata</a>
<router-link to="/admin/reset_metadata">Manage Metadata</router-link>
</strong>
- Select on which repositories you want to reset metadata.
</li>
<li>
<strong>
<a :href="adminToolVersionsUrl" @click.prevent="useRouter">View Lineage</a>
<router-link to="/admin/tool_versions">View Lineage</router-link>
</strong>
- A view of a version lineages for all installed tools. Useful for debugging.
</li>
@@ -116,42 +116,11 @@
</template>
<script>
import { getAppRoot } from "onload/loadConfig";
import { getGalaxyInstance } from "app";
const root = getAppRoot();
export default {
props: {
isRepoInstalled: {
isToolshedInstalled: {
type: Boolean,
required: true,
},
isToolShedInstalled: {
type: Boolean,
required: true,
},
},
computed: {
adminDataTypesUrl: () => `${root}admin/data_types`,
adminDataTablesUrl: () => `${root}admin/data_tables`,
adminDisplayApplicationsUrl: () => `${root}admin/display_applications`,
adminJobsUrl: () => `${root}admin/jobs`,
adminManageMetadata: () => `${root}admin/reset_metadata`,
adminDMUrl: () => `${root}admin/data_manager`,
adminUsersUrl: () => `${root}admin/users`,
adminQuotasUrl: () => `${root}admin/quotas`,
adminGroupsUrl: () => `${root}admin/groups`,
adminRolesUrl: () => `${root}admin/roles`,
adminSanitizeAllowUrl: () => `${root}admin/sanitize_allow`,
adminFormsUrl: () => `${root}admin/forms`,
adminToolshedUrl: () => `${root}admin/toolshed`,
adminToolVersionsUrl: () => `${root}admin/tool_versions`,
},
methods: {
useRouter: function (ev) {
const Galaxy = getGalaxyInstance();
Galaxy.page.router.push(ev.target.pathname.slice(root.length));
default: false,
},
},
};
@@ -1,6 +1,8 @@
<template>
<b-form @submit.prevent="submit">
<b-alert :show="messageShow" :variant="messageVariant" v-html="messageText" />
<b-alert :show="messageShow" :variant="messageVariant">
{{ messageText }}
</b-alert>
<b-card header="Change your password">
<b-form-group v-if="user" label="Current Password">
<b-form-input v-model="current" type="password" />
+4 -4
View File
@@ -3,7 +3,9 @@
<div class="row justify-content-md-center">
<template v-if="!confirmURL">
<div class="col col-lg-6">
<b-alert :show="messageShow" :variant="messageVariant" v-html="messageText" />
<b-alert :show="messageShow" :variant="messageVariant">
{{ messageText }}
</b-alert>
<b-form id="login" @submit.prevent="submitGalaxyLogin()">
<b-card no-body header="Welcome to Galaxy, please log in">
<b-card-body>
@@ -125,9 +127,7 @@ export default {
},
methods: {
toggleLogin() {
if (this.$root.toggleLogin) {
this.$root.toggleLogin();
}
this.$emit("toggle-login");
},
submitGalaxyLogin(method) {
if (localStorage.getItem("redirect_url")) {
+6 -2
View File
@@ -1,14 +1,18 @@
<template>
<div>
<template v-if="showLogin">
<login-form :show_welcome_with_login="show_welcome_with_login" :welcome_url="welcome_url" />
<login-form
:show_welcome_with_login="show_welcome_with_login"
:welcome_url="welcome_url"
@toggle-login="toggleLogin" />
</template>
<template v-else>
<register-form
:registration_warning_message="registration_warning_message"
:mailing_join_addr="mailing_join_addr"
:server_mail_configured="server_mail_configured"
:terms_url="terms_url" />
:terms_url="terms_url"
@toggle-login="toggleLogin" />
</template>
</div>
</template>

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