diff --git a/.ci/flake8_blacklist.txt b/.ci/flake8_blacklist.txt index 33db66f40a0..26ece30af32 100644 --- a/.ci/flake8_blacklist.txt +++ b/.ci/flake8_blacklist.txt @@ -2,6 +2,10 @@ .tox .venv .venv3 +packages/*/.venv +packages/*/build +packages/*/dist +packages/venv node_modules database doc/build diff --git a/.ci/jenkins/selenium-compose/Dockerfile b/.ci/jenkins/selenium-compose/Dockerfile index a453ac6d215..d9f3def9761 100644 --- a/.ci/jenkins/selenium-compose/Dockerfile +++ b/.ci/jenkins/selenium-compose/Dockerfile @@ -1,5 +1,5 @@ FROM toolshed/requirements -MAINTAINER John Chilton, jmchilton@gmail.com +LABEL maintainer="John Chilton " RUN apt-get -qq update && \ apt-get install --no-install-recommends -y postgresql-client python-pip libffi-dev python-cffi && \ diff --git a/.ci/validate_test_tools.sh b/.ci/validate_test_tools.sh index 9b43e7a6c46..258799dc95d 100755 --- a/.ci/validate_test_tools.sh +++ b/.ci/validate_test_tools.sh @@ -1,11 +1,11 @@ #!/bin/sh -cd `dirname $0`/.. +cd "$(dirname "$0")"/.. xsd_path="lib/galaxy/tools/xsd/galaxy.xsd" # Lint the XSD xmllint --noout "$xsd_path" test_tools_path='test/functional/tools' -tool_files_list=$(ls "$test_tools_path"/*xml | grep -v '_conf.xml$') +tool_files_list=$(ls "$test_tools_path"/*.xml | grep -v '_conf.xml$') sh scripts/validate_tools.sh $tool_files_list diff --git a/.circleci/config.yml b/.circleci/config.yml index 01b3105991a..32dfbce7036 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -25,7 +25,7 @@ variables: jobs: get_code: docker: - - image: circleci/python:2.7.15 + - image: circleci/python:3.5 <<: *set_workdir steps: # Replace standard code checkout with shallow clone to speed things up. @@ -83,7 +83,7 @@ jobs: - ~/repo py27_lint: docker: - - image: circleci/python:2.7.15 + - image: circleci/python:2.7 <<: *set_workdir steps: - *restore_repo_cache @@ -91,7 +91,7 @@ jobs: - run: tox -e py27-lint py27_unit: docker: - - image: circleci/python:2.7.15 + - image: circleci/python:2.7 <<: *set_workdir steps: - *restore_repo_cache @@ -99,7 +99,7 @@ jobs: - run: tox -e py27-unit py27_docstring: docker: - - image: circleci/python:2.7.15 + - image: circleci/python:2.7 <<: *set_workdir steps: - *restore_repo_cache @@ -107,7 +107,7 @@ jobs: - run: tox -e py27-lint_docstring_include_list py27_first_startup: docker: - - image: circleci/python:2.7.15 + - image: circleci/python:2.7 <<: *set_workdir steps: - *restore_repo_cache @@ -139,14 +139,14 @@ jobs: <<: *set_workdir steps: - *restore_repo_cache - - *install_tox - run: sudo apt-get update # For uwsgi - run: sudo apt-get install -y libpython3.5-dev + - *install_tox - run: tox -e py35-first_startup validate_test_tools: docker: - - image: circleci/python:2.7.15 + - image: circleci/python:3.5 <<: *set_workdir steps: - *restore_repo_cache @@ -154,9 +154,17 @@ jobs: - run: sudo apt-get install -y libxml2-utils - *install_tox - run: tox -e validate_test_tools + test_galaxy_packages: + docker: + - image: circleci/python:3.5 + <<: *set_workdir + steps: + - *restore_repo_cache + - *install_tox + - run: tox -e test_galaxy_packages check_py3_compatibility: docker: - - image: circleci/python:2.7.15 + - image: circleci/python:3.5 <<: *set_workdir steps: - *restore_repo_cache @@ -202,6 +210,8 @@ workflows: <<: *requires_get_code - py35_first_startup: <<: *requires_get_code + - test_galaxy_packages: + <<: *requires_get_code - validate_test_tools: <<: *requires_get_code - check_py3_compatibility: diff --git a/client/.eslintrc.js b/client/.eslintrc.js index 5b5c90440a8..7dc40777623 100644 --- a/client/.eslintrc.js +++ b/client/.eslintrc.js @@ -1,13 +1,18 @@ module.exports = { extends: ["eslint:recommended", "plugin:vue/essential"], // airbnb-base, eventually env: { - browser: true + browser: true, + commonjs: true, + es6: true, + node: true, + mocha: true }, parserOptions: { parser: "babel-eslint" }, plugins: ["html"], rules: { "no-console": "off", "no-unused-vars": ["error", { args: "none" }], + "prefer-const": "error", // I'd love to turn on camelcase, but it's a big shift with tons of current errors. // camelcase: [ // "error", @@ -15,5 +20,10 @@ module.exports = { // properties: "always" // } // ] + }, + globals: { + // chai tests + assert: true, + expect: true } }; diff --git a/client/README.md b/client/README.md index 3094d6b69d7..b312c525c3d 100644 --- a/client/README.md +++ b/client/README.md @@ -8,8 +8,8 @@ the client as a part of server startup, when it detects changes, unless that functionality is explicitly disabled. The base dependencies used are Node.js and Yarn. Galaxy now includes these in -the virtual environment, and they can be accessed by activating that with -`. .venv/bin/activate` from the Galaxy root directory. +the virtual environment, and they can be accessed by activating that with `. +.venv/bin/activate` from the Galaxy root directory. If you'd like to install your own dependencies, on OSX the easiest way to get set up is using `homebrew` and the command `brew install nodejs yarn`. More @@ -18,10 +18,9 @@ https://nodejs.org/ and https://yarnpkg.com/ . The Galaxy client build has necessarily grown more complex in the past several years, but we are still trying to keep things as simple as possible for -everyone. If you're having any trouble with building the -client after following the instructions below, please create an issue on GitHub -or reach out for help directly on Gitter at -https://gitter.im/galaxyproject/Lobby . +everyone. If you're having any trouble with building the client after +following the instructions below, please create an issue on GitHub or reach out +for help directly on Gitter at https://gitter.im/galaxyproject/Lobby . Complete Client Build @@ -37,7 +36,8 @@ during the build process. make client -For a production build, suitable for deploying to a live server, use the following: +For a production build, suitable for deploying to a live server, use the +following: make client-production @@ -80,3 +80,219 @@ On build, the compiled css bundle is served at `/static/style/base.css`. As mentioned above, `make client` will rebuild styles, as a part of the webpack build. For iterative development, "Watch Mode" rebuilds as described above do include style changes. + + +# Client-Side Unit Testing + +Galaxy's client is undergoing an extensive refactoring and modernizing process. +As part of this initiative, we would like to request that all new client-side +code submissions come with accompanying javascript unit-tests for the +developer-facing API of your new code. + +## Testing Technologies + +[Galaxy uses Mocha](https://mochajs.org/) for its client-side unit testing +framework with [Chai as its assertion library](https://www.chaijs.com/) and +[karma as a test runner](https://karma-runner.github.io/latest/index.html). +The karma configs are contained in the client/karma folder. Unit tests are run +using Chrome headless by default. + +For testing Vue components, we use the [Vue testing +utils](https://vue-test-utils.vuejs.org/) to mount individual components in a +test bed and check them for rendered features. + +We use the [babel plugin +Rewire](https://github.com/speedskater/babel-plugin-rewire) to mock imported +ES6 dependencies during unit-testing. [Sinon](https://sinonjs.org/) is also +available for more robust stubbing, mocking and spying utilities. + +A set of older qUnit tests also exist which will likely be phased-out as the +code they support is replaced with modern component-based implementations. In +the meantime, karma runs both the mocha and qunit tests in sequence. + + +## Running the tests + +### At Build-Time +To simply run all the javascript unit tests, you can use make from the root +directory. This is what happens during a complete client build. + + make client-test + + +### During Development + +During client-side development, it is more convenient to have granular testing +options. The various testing scripts are defined inside package.json within the +client folder, and are called either with npm (or yarn) as follows: + + +#### Run all tests (mocha and qunit): + + npm run test + +#### Watch and rerun all client-side unit-tests every time a source file changes: + + npm run test-watch + +#### Run only specified test files when a source file changes: + + npm run test-watch watch-only="Tags/*.test.js" + +(The above watch-only parameter will be mapped to the following glob expression "**/Tags/*.test.js") + +#### Run qunit legacy tests + + npm run test-qunit + + +## Writing a test file + +The karma configs are setup to look at any file ending in "*.test.js". Please +place your test files inside client/galaxy/scripts folders right next to +whatever files that they are testing. + +```javascript +// yourtestfile.test.js + +import { things } from "./yourtestfile.js"; + +describe("some module you wrote", () => { + + let transientVariables, serviceInstances, testData; + + beforeEach(() => { + // setup your code (if necessary) + }) + + afterEach(() => { + // teardown your code (so it doesn't ruin the next test) + }) + + it("should do something or other", () => { + assert(somethingThatShouldTrue, "explain what went wrong"); + expect(something).to.eq(true); + }) +}) +``` + + +## Testing ~~Suggestions~~ Obligations + + +### Clearly document the intent of your test + +Please remember that these tests are not *for* you. They're for the people who +come after you. It will be a lot easier to modify, repair and upgrade your code +if they can figure out what you were originally hoping to accomplish. + +Add a couple of comments. Use variable names that mean something. Use the +assertion failure description parameters when possible. Nobody's code is as +self-documenting as they believe it to be. + +```javascript + +// NOT AWESOME +// this test forces the next victim to have to +// read every single line of source code you wrote +it("should work", async () => { + let thing = await svc.loadHistories(); + assert(thing); +}) + +// BETTER +// With this test, you can just read at the errors on the mocha +// test output to have a pretty good idea of what went sour. +it("should have resolved to an array of integer IDs", async () => { + let historyIds = await historyService.loadHistories(); + assert(historyIds, "History service should have had a result"); + assert(historyIds instanceof Array, "History results should have been an array"); + assert(historyIds.length == 4, "History call should have returned 4 ids"); +}) + +``` + + +### Only test the public API that you define (carefully!) + +Internal implementations come and go with library upgrades and new tech. But +the point of the unit test is to make sure your units work as designed.... +which means you need to... you know... design your code to work in units. + +Separate your concerns and identify the developer-facing methods and functions +you expect them to use. Test THOSE. Everything else should probably be +considered an implementation detail. + +Assume nobody cares *how* your code works, we just need to know that the public +API you designed *does* work. If performance problems or new tech necessitate a +re-write, these tests become a guide for the next implementation. + + +### Wrap native browser resources in a function so they can be easily mocked + +If your javascript needs to talk to the window object, or navigator, etc. wrap +that in a function call so that it can be easily mocked during testing. + +```javascript + +// myModule.js + +// ... other code + +export function redirectTo(url) { + window.location = url; +} + + +// myModule.test.js + +import { __RewireAPI__ as myModuleRewire} from "./myModule"; + +function fakeRedirect(url) { + console.log(`I would have gone to: ${url}`); +} + +describe("some module", () => { + + beforeEach(() => { + myModuleRewire.__Rewire__("redirectTo", fakeRedirect); + }) + + // .... +}) +``` + +### Implement logic in pure functions when possible + +The more of your logic that is written in deterministic functions (i.e. no +side-effects, same inputs always result in same outputs) the easier it is to +test. Just load up the functions and supply suitable test inputs. + +There is almost definitely no such thing as a well-written 1000 line function. +Most of whatever happened in that thing was probably deterministic and can be +broken up into easily testable chunks. + + +## Specific test scenarios & examples + +[Mocking an imported dependency](https://github.com/galaxyproject/galaxy/blob/dev/client/galaxy/scripts/components/Tags/tagService.test.js) + +[Testing async operations](https://github.com/galaxyproject/galaxy/blob/dev/client/galaxy/scripts/components/Tags/tagService.test.js) + +[Testing a Vue component for expected rendering output](https://github.com/galaxyproject/galaxy/blob/dev/client/galaxy/scripts/components/Tags/StatelessTags.test.js) + +[Firing an event against a shallow mounted vue component](https://github.com/galaxyproject/galaxy/blob/dev/client/galaxy/scripts/components/Tags/StatelessTags.test.js) + + +## The dirty secret about testing + +It's good to have tests, but testing code isn't really about testing at all, +it's actually about software design. + +Maintainable software is testable software. If you can run a unit test on your +code, then that means you must have necessarily separated your code into +testable units and you will have almost definitely written better, more +modular, more easily manipulated code, and nobody will ever contemplate using +git-blame to figure out what went wrong. + +Probably. diff --git a/client/galaxy/scripts/admin.toolshed.js b/client/galaxy/scripts/admin.toolshed.js index 8a94d153909..04182abfda5 100644 --- a/client/galaxy/scripts/admin.toolshed.js +++ b/client/galaxy/scripts/admin.toolshed.js @@ -9,19 +9,6 @@ import mod_repo_status_view from "mvc/toolshed/repo-status-view"; import mod_workflows_view from "mvc/toolshed/workflows-view"; var AdminToolshedRouter = Backbone.Router.extend({ - initialize: function() { - this.routesHit = 0; - // keep count of number of routes handled by the application - Backbone.history.on( - "route", - function() { - this.routesHit++; - }, - this - ); - this.bind("route", this.trackPageview); - }, - routes: { "": "toolsheds", sheds: "toolsheds", @@ -32,20 +19,6 @@ var AdminToolshedRouter = Backbone.Router.extend({ "categories/s/:tool_shed": "categories", "category/s/:tool_shed/c/:category_id/k/:sort_key/p/:page/t/:sort_order": "repositories", "repository/s/:tool_shed/r/:repository_id": "repository" - }, - - /** - * If more than one route has been hit the user did not land on current - * page directly so we can go back safely. Otherwise go to the home page. - * Use replaceState if available so the navigation doesn't create an - * extra history entry - */ - back: function() { - if (this.routesHit > 1) { - window.history.back(); - } else { - this.navigate("#", { trigger: true, replace: true }); - } } }); @@ -55,7 +28,7 @@ var GalaxyAdminToolshedApp = Backbone.View.extend({ }, initialize: function() { - let Galaxy = getGalaxyInstance(); + const Galaxy = getGalaxyInstance(); Galaxy.admintoolshedapp = this; this.admin_toolshed_router = new AdminToolshedRouter(); diff --git a/client/galaxy/scripts/app/galaxy.js b/client/galaxy/scripts/app/galaxy.js index 3a43a178877..b9d547f103a 100644 --- a/client/galaxy/scripts/app/galaxy.js +++ b/client/galaxy/scripts/app/galaxy.js @@ -26,12 +26,12 @@ export function GalaxyApp(options = {}, bootstrapped = {}) { addLogging(GalaxyApp, "GalaxyApp"); // a debug flag can be set via local storage and made available during script/page loading -var DEBUGGING_KEY = "galaxy:debug"; +const DEBUGGING_KEY = "galaxy:debug"; -var NAMESPACE_KEY = `${DEBUGGING_KEY}:namespaces`; -var FLATTEN_LOG_MESSAGES_KEY = `${DEBUGGING_KEY}:flatten`; +const NAMESPACE_KEY = `${DEBUGGING_KEY}:namespaces`; +const FLATTEN_LOG_MESSAGES_KEY = `${DEBUGGING_KEY}:flatten`; -var localDebugging = false; +let localDebugging = false; try { localDebugging = localStorage.getItem(DEBUGGING_KEY) == "true"; } catch (storageErr) { @@ -40,51 +40,50 @@ try { /** initalize options and sub-components */ GalaxyApp.prototype._init = function(options, bootstrapped) { - var self = this; - _.extend(self, Backbone.Events); + _.extend(this, Backbone.Events); if (localDebugging) { - self.logger = console; + this.logger = console; console.debug("debugging galaxy:", "options:", options, "bootstrapped:", bootstrapped); } - self._processOptions(options); - self._initConfig(options.config || {}); + this._processOptions(options); + this._initConfig(options.config || {}); // Patch if this galaxy instance is replacing an existing one // _patchGalaxy depends on options.patchExisting // TODO: rethink this behavior - let existingGalaxy = getGalaxyInstance(); + const existingGalaxy = getGalaxyInstance(); if (existingGalaxy) { - self._patchGalaxy(existingGalaxy); + this._patchGalaxy(existingGalaxy); } // add root and url parameters - self.root = options.root || "/"; - self.params = options.params || {}; - self.session_csrf_token = options.session_csrf_token || null; + this.root = options.root || "/"; + this.params = options.params || {}; + this.session_csrf_token = options.session_csrf_token || null; - self._initLogger(self.options.loggerOptions || {}); + this._initLogger(this.options.loggerOptions || {}); // at this point, either logging or not and namespaces are enabled - chat it up - self.debug("GalaxyApp.options: ", self.options); - self.debug("GalaxyApp.config: ", self.config); - self.debug("GalaxyApp.logger: ", self.logger); + this.debug("GalaxyApp.options: ", this.options); + this.debug("GalaxyApp.config: ", this.config); + this.debug("GalaxyApp.logger: ", this.logger); - self._initLocale(); - self.debug("GalaxyApp.localize: ", self.localize); + this._initLocale(); + this.debug("GalaxyApp.localize: ", this.localize); - self.config = options.config || {}; - self.debug("GalaxyApp.config: ", self.config); + this.config = options.config || {}; + this.debug("GalaxyApp.config: ", this.config); - self._initUser(options.user || {}); - self.debug("GalaxyApp.user: ", self.user); + this._initUser(options.user || {}); + this.debug("GalaxyApp.user: ", this.user); - self._initUserLocale(); - self.debug("currentLocale: ", sessionStorage.getItem("currentLocale")); + this._initUserLocale(); + this.debug("currentLocale: ", sessionStorage.getItem("currentLocale")); - self._setUpListeners(); - self.trigger("ready", self); + this._setUpListeners(); + this.trigger("ready", this); - return self; + return this; }; /** default options */ @@ -98,41 +97,38 @@ GalaxyApp.prototype.defaultOptions = { /** filter to options present in defaultOptions (and default to them) */ GalaxyApp.prototype._processOptions = function _processOptions(options) { - var self = this; - var defaults = self.defaultOptions; + const defaults = this.defaultOptions; - self.options = {}; - for (var k in defaults) { + this.options = {}; + for (const k in defaults) { if (defaults.hasOwnProperty(k)) { - self.options[k] = options.hasOwnProperty(k) ? options[k] : defaults[k]; + this.options[k] = options.hasOwnProperty(k) ? options[k] : defaults[k]; } } - return self; + return this; }; /** parse the config and any extra info derived from it */ GalaxyApp.prototype._initConfig = function _initConfig(config) { - var self = this; - self.config = config; + this.config = config; // give precendence to localdebugging for this setting - self.config.debug = localDebugging || self.config.debug; + this.config.debug = localDebugging || this.config.debug; - return self; + return this; }; // TODO: Remove this behavior when we can, it is kind of non-intuitive GalaxyApp.prototype._patchGalaxy = function _patchGalaxy(patchWith) { - var self = this; // in case req or plain script tag order has created a prev. version of the Galaxy obj... - if (self.options.patchExisting && patchWith) { - // self.debug( 'found existing Galaxy object:', patchWith ); + if (this.options.patchExisting && patchWith) { + // this.debug( 'found existing Galaxy object:', patchWith ); // ...(for now) monkey patch any added attributes that the previous Galaxy may have had //TODO: move those attributes to more formal assignment in GalaxyApp - for (var k in patchWith) { + for (const k in patchWith) { if (patchWith.hasOwnProperty(k)) { - // self.debug( '\t patching in ' + k + ' to Galaxy:', self[ k ] ); - self[k] = patchWith[k]; + // this.debug( '\t patching in ' + k + ' to Galaxy:', this[ k ] ); + this[k] = patchWith[k]; } } } @@ -140,10 +136,8 @@ GalaxyApp.prototype._patchGalaxy = function _patchGalaxy(patchWith) { /** set up the metrics logger (utils/metrics-logger) and pass loggerOptions */ GalaxyApp.prototype._initLogger = function _initLogger(loggerOptions) { - var self = this; - // default to console logging at the debug level if the debug flag is set - if (self.config.debug) { + if (this.config.debug) { loggerOptions.consoleLogger = loggerOptions.consoleLogger || console; loggerOptions.consoleLevel = loggerOptions.consoleLevel || metricsLogger.MetricsLogger.ALL; // load any logging namespaces from localStorage if we can @@ -160,45 +154,42 @@ GalaxyApp.prototype._initLogger = function _initLogger(loggerOptions) { console.log(loggerOptions.consoleFlattenMessages); } - self.logger = new metricsLogger.MetricsLogger(loggerOptions); - self.emit = {}; + this.logger = new metricsLogger.MetricsLogger(loggerOptions); + this.emit = {}; ["log", "debug", "info", "warn", "error", "metric"].map(i => { - self.emit[i] = function(data) { - self.logger.emit(i, arguments[0], Array.prototype.slice.call(arguments, 1)); + this.emit[i] = data => { + this.logger.emit(i, arguments[0], Array.prototype.slice.call(arguments, 1)); }; }); - if (self.config.debug) { + if (this.config.debug) { // add this logger to mvc's loggable mixin so that all models can use the logger - BASE_MVC.LoggableMixin.logger = self.logger; + BASE_MVC.LoggableMixin.logger = this.logger; } - return self; + return this; }; /** add the localize fn to this object and the window namespace (as '_l') */ GalaxyApp.prototype._initLocale = function _initLocale(options) { - var self = this; - self.debug("_initLocale:", options); - self.localize = localize; + this.debug("_initLocale:", options); + this.localize = localize; // add to window as global shortened alias // TODO: temporary - remove when can require for plugins - window._l = self.localize; - return self; + window._l = this.localize; + return this; }; /** add the localize fn to this object and the window namespace (as '_l') */ GalaxyApp.prototype._initUserLocale = function _initUserLocale(options) { - var self = this; - // Choose best locale - var global_locale = self.config.default_locale ? self.config.default_locale.toLowerCase() : false; + const global_locale = this.config.default_locale ? this.config.default_locale.toLowerCase() : false; - var extra_user_preferences = {}; - if (self.user && self.user.attributes.preferences && "extra_user_preferences" in self.user.attributes.preferences) { - extra_user_preferences = JSON.parse(self.user.attributes.preferences.extra_user_preferences); + let extra_user_preferences = {}; + if (this.user && this.user.attributes.preferences && "extra_user_preferences" in this.user.attributes.preferences) { + extra_user_preferences = JSON.parse(this.user.attributes.preferences.extra_user_preferences); } - var user_locale = + let user_locale = "localization|locale" in extra_user_preferences ? extra_user_preferences["localization|locale"].toLowerCase() : false; @@ -207,52 +198,48 @@ GalaxyApp.prototype._initUserLocale = function _initUserLocale(options) { user_locale = false; } - var nav_locale = + const nav_locale = typeof navigator === "undefined" ? "__root" : (navigator.language || navigator.userLanguage || "__root").toLowerCase(); - let locale = user_locale || global_locale || nav_locale; + const locale = user_locale || global_locale || nav_locale; sessionStorage.setItem("currentLocale", locale); }; /** set up the current user as a Backbone model (mvc/user/user-model) */ GalaxyApp.prototype._initUser = function _initUser(userJSON) { - var self = this; - self.debug("_initUser:", userJSON); - self.user = new userModel.User(userJSON); - self.user.logger = self.logger; - return self; + this.debug("_initUser:", userJSON); + this.user = new userModel.User(userJSON); + this.user.logger = this.logger; + return this; }; /** Set up DOM/jQuery/Backbone event listeners enabled for all pages */ GalaxyApp.prototype._setUpListeners = function _setUpListeners() { - var self = this; - // hook to jq beforeSend to record the most recent ajax call and cache some data about it /** cached info about the last ajax call made through jQuery */ - self.lastAjax = {}; + this.lastAjax = {}; $(document).bind("ajaxSend", (ev, xhr, options) => { - var data = options.data; + let data = options.data; try { data = JSON.parse(data); } catch (err) { // data isn't JSON, skip. } - self.lastAjax = { + this.lastAjax = { url: location.href.slice(0, -1) + options.url, data: data }; //TODO:?? we might somehow manage to *retry* ajax using either this hook or Backbone.sync }); - return self; + return this; }; /** Turn debugging/console-output on/off by passing boolean. Pass nothing to get current setting. */ GalaxyApp.prototype.debugging = function _debugging(setting) { - var self = this; try { if (setting === undefined) { return localStorage.getItem(DEBUGGING_KEY) === "true"; @@ -264,7 +251,7 @@ GalaxyApp.prototype.debugging = function _debugging(setting) { localStorage.removeItem(DEBUGGING_KEY); // also remove all namespaces - self.debuggingNamespaces(null); + this.debuggingNamespaces(null); } catch (storageErr) { console.log(localize("localStorage not available for debug flag retrieval")); } @@ -278,19 +265,18 @@ GalaxyApp.prototype.debugging = function _debugging(setting) { * Returns the new/current namespaces as an array; */ GalaxyApp.prototype.debuggingNamespaces = function _debuggingNamespaces(namespaces) { - var self = this; try { if (namespaces === undefined) { - var csv = localStorage.getItem(NAMESPACE_KEY); + const csv = localStorage.getItem(NAMESPACE_KEY); return typeof csv === "string" ? csv.split(",") : []; } else if (namespaces === null) { localStorage.removeItem(NAMESPACE_KEY); } else { localStorage.setItem(NAMESPACE_KEY, namespaces); } - var newSettings = self.debuggingNamespaces(); - if (self.logger) { - self.logger.options.consoleNamespaceWhitelist = newSettings; + const newSettings = this.debuggingNamespaces(); + if (this.logger) { + this.logger.options.consoleNamespaceWhitelist = newSettings; } return newSettings; } catch (storageErr) { @@ -300,7 +286,7 @@ GalaxyApp.prototype.debuggingNamespaces = function _debuggingNamespaces(namespac /** string rep */ GalaxyApp.prototype.toString = function toString() { - var userEmail = this.user ? this.user.get("email") || "(anonymous)" : "uninitialized"; + const userEmail = this.user ? this.user.get("email") || "(anonymous)" : "uninitialized"; return `GalaxyApp(${userEmail})`; }; diff --git a/client/galaxy/scripts/app/singleton.js b/client/galaxy/scripts/app/singleton.js index 482ffbb6391..fa96b99f734 100644 --- a/client/galaxy/scripts/app/singleton.js +++ b/client/galaxy/scripts/app/singleton.js @@ -9,7 +9,7 @@ import { serverPath } from "utils/serverPath"; export function setGalaxyInstance(factory) { console.warn("setGalaxyInstance", serverPath()); - let storage = getStorage(); + const storage = getStorage(); let newInstance = factory(GalaxyApp); if (!(newInstance instanceof GalaxyApp)) { newInstance = new GalaxyApp(newInstance); @@ -24,12 +24,12 @@ export function setGalaxyInstance(factory) { } export function getGalaxyInstance() { - let storage = getStorage(); + const storage = getStorage(); return storage._galaxyInstance; } export function galaxyIsInitialized() { - let instance = getGalaxyInstance(); + const instance = getGalaxyInstance(); return instance !== null; } diff --git a/client/galaxy/scripts/bundleEntries.js b/client/galaxy/scripts/bundleEntries.js index be5c973607f..0390749fb83 100644 --- a/client/galaxy/scripts/bundleEntries.js +++ b/client/galaxy/scripts/bundleEntries.js @@ -32,6 +32,9 @@ export { default as HDAModel } from "mvc/history/hda-model"; export { default as LegacyGridView } from "legacy/grid/grid-view"; export { create_chart, create_histogram } from "reports/run_stats"; export { default as ToolshedGroups } from "toolshed/toolshed.groups"; +export { default as IES } from "galaxy.interactive_environments"; + +export { Toast } from "ui/toast"; // TODO: remove when external consumers are updated/gone (IES right now) export function adminToolshed(options) { new AdminToolshed.GalaxyApp(options); @@ -54,14 +57,14 @@ export function library(options) { } export function multiHistory(options) { - let histories = new HistoryCollection([], { + const histories = new HistoryCollection([], { includeDeleted: options.includingDeleted, order: options.order, limitOnFirstFetch: options.limit, limitPerFetch: options.limit, currentHistoryId: options.current_history_id }); - let multipanel = new MultiPanel.MultiPanelColumns({ + const multipanel = new MultiPanel.MultiPanelColumns({ el: $("#center").get(0), histories: histories }); @@ -76,6 +79,7 @@ export function multiHistory(options) { 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"; export { default as async_save_text } from "utils/async-save-text"; // Previously "chart" @@ -88,7 +92,7 @@ export function chart(options) { return new Client(options); } -export let chartUtilities = { +export const chartUtilities = { Datasets: Datasets, Jobs: Jobs, Series: Series @@ -97,6 +101,8 @@ export let chartUtilities = { export { initMasthead } from "components/Masthead/initMasthead"; export { panelManagement } from "onload/globalInits/panelManagement"; export { mountMakoTags } from "components/Tags"; +export { mountJobMetrics } from "components/JobMetrics"; +export { mountJobParameters } from "components/JobParameters"; // Used in common.mako export { default as store } from "storemodern"; diff --git a/client/galaxy/scripts/components/Citations.vue b/client/galaxy/scripts/components/Citations.vue index 86d9af002f8..46c960f3b9f 100644 --- a/client/galaxy/scripts/components/Citations.vue +++ b/client/galaxy/scripts/components/Citations.vue @@ -29,12 +29,16 @@ diff --git a/client/galaxy/scripts/components/JobMetrics/index.js b/client/galaxy/scripts/components/JobMetrics/index.js new file mode 100644 index 00000000000..416a80e8339 --- /dev/null +++ b/client/galaxy/scripts/components/JobMetrics/index.js @@ -0,0 +1,4 @@ +export { default as JobMetrics } from "./JobMetrics"; + +// functions for mounting job metrics in non-Vue environments +export { mountJobMetrics } from "./mount"; diff --git a/client/galaxy/scripts/components/JobMetrics/mount.js b/client/galaxy/scripts/components/JobMetrics/mount.js new file mode 100644 index 00000000000..04b1bfe0c57 --- /dev/null +++ b/client/galaxy/scripts/components/JobMetrics/mount.js @@ -0,0 +1,18 @@ +/** + * Endpoint for mounting job metrics from non-Vue environment. + */ +import $ from "jquery"; +import JobMetrics from "./JobMetrics.vue"; +import { mountVueComponent } from "utils/mountVueComponent"; + +export const mountJobMetrics = (propsData = {}) => { + $(".job-metrics").each((index, el) => { + const jobId = $(el).attr("job_id"); + const datasetId = $(el).attr("dataset_id"); + const datasetType = $(el).attr("dataset_type") || "hda"; + propsData.jobId = jobId; + propsData.datasetId = datasetId; + propsData.datasetType = datasetType; + mountVueComponent(JobMetrics)(propsData, el); + }); +}; diff --git a/client/galaxy/scripts/components/JobParameters/JobParameters.vue b/client/galaxy/scripts/components/JobParameters/JobParameters.vue new file mode 100644 index 00000000000..0d63d32773c --- /dev/null +++ b/client/galaxy/scripts/components/JobParameters/JobParameters.vue @@ -0,0 +1,121 @@ + + + + diff --git a/client/galaxy/scripts/components/JobParameters/index.js b/client/galaxy/scripts/components/JobParameters/index.js new file mode 100644 index 00000000000..8d76729387f --- /dev/null +++ b/client/galaxy/scripts/components/JobParameters/index.js @@ -0,0 +1,4 @@ +export { default as JobParameters } from "./JobParameters"; + +// functions for mounting job parameters display in non-Vue environments +export { mountJobParameters } from "./mount"; diff --git a/client/galaxy/scripts/components/JobParameters/mount.js b/client/galaxy/scripts/components/JobParameters/mount.js new file mode 100644 index 00000000000..4e7e9f67fb6 --- /dev/null +++ b/client/galaxy/scripts/components/JobParameters/mount.js @@ -0,0 +1,19 @@ +/** + * Endpoint for mounting job parameters from non-Vue environment. + */ +import $ from "jquery"; +import Vue from "vue"; +import JobParameters from "./JobParameters.vue"; + +export const mountJobParameters = (propsData = {}) => { + $(".job-parameters").each((index, el) => { + const jobId = $(el).attr("job_id"); + const datasetId = $(el).attr("dataset_id"); + const datasetType = $(el).attr("dataset_type") || "hda"; + const component = Vue.extend(JobParameters); + propsData.jobId = jobId; + propsData.datasetId = datasetId; + propsData.datasetType = datasetType; + return new component({ propsData: propsData }).$mount(el); + }); +}; diff --git a/client/galaxy/scripts/components/MaskedInput.js b/client/galaxy/scripts/components/MaskedInput.js new file mode 100644 index 00000000000..7cac27045eb --- /dev/null +++ b/client/galaxy/scripts/components/MaskedInput.js @@ -0,0 +1,29 @@ +import BInput from "bootstrap-vue/es/components/form-input/form-input"; +import { createMask } from "imask"; + +export default { + extends: BInput, + props: { + mask: { + type: String, + required: false, + default: "" + } + }, + computed: { + masker() { + return createMask({ + mask: this.mask + }); + } + }, + methods: { + getFormatted(value) { + const result = this.stringifyValue(value); + if (this.mask.length) { + return this.masker.resolve(result); + } + return result; + } + } +}; diff --git a/client/galaxy/scripts/components/Masthead/index.vue b/client/galaxy/scripts/components/Masthead/index.vue index ba0b6f6ba08..f232e2302b0 100644 --- a/client/galaxy/scripts/components/Masthead/index.vue +++ b/client/galaxy/scripts/components/Masthead/index.vue @@ -22,7 +22,7 @@ export default { return; } - let galaxy = getGalaxyInstance(); + const galaxy = getGalaxyInstance(); // old backbone view if (!galaxy.masthead) { @@ -30,7 +30,7 @@ export default { } // bolt the rendered DOM onto the vue component's element - let backboneDom = galaxy.masthead.render(); + const backboneDom = galaxy.masthead.render(); this.$el.appendChild(backboneDom.$el[0]); } } diff --git a/client/galaxy/scripts/components/Masthead/initMasthead.js b/client/galaxy/scripts/components/Masthead/initMasthead.js index c8b69693cae..9df6e15e230 100644 --- a/client/galaxy/scripts/components/Masthead/initMasthead.js +++ b/client/galaxy/scripts/components/Masthead/initMasthead.js @@ -7,7 +7,7 @@ import Masthead from "components/Masthead"; export function initMasthead(config, container) { console.log("initMasthead"); - let Component = Vue.extend(Masthead); + const Component = Vue.extend(Masthead); return new Component({ props: Object.keys(config), propsData: config, diff --git a/client/galaxy/scripts/components/Nametags/Nametag.vue b/client/galaxy/scripts/components/Nametags/Nametag.vue index 183db221f7b..89cfc4a6251 100644 --- a/client/galaxy/scripts/components/Nametags/Nametag.vue +++ b/client/galaxy/scripts/components/Nametags/Nametag.vue @@ -14,7 +14,7 @@ export default { return this.tag.startsWith("name:") ? this.tag.replace("name:", "") : this.tag; }, tagStyles() { - let { primary, contrasting, darker } = keyedColorScheme(this.tag); + const { primary, contrasting, darker } = keyedColorScheme(this.tag); return { "background-color": primary, color: contrasting, diff --git a/client/galaxy/scripts/components/PageEditor/PageEditor.vue b/client/galaxy/scripts/components/PageEditor/PageEditor.vue new file mode 100644 index 00000000000..f589b15e23a --- /dev/null +++ b/client/galaxy/scripts/components/PageEditor/PageEditor.vue @@ -0,0 +1,38 @@ + + + diff --git a/client/galaxy/scripts/components/PageEditor/PageEditorHtml.vue b/client/galaxy/scripts/components/PageEditor/PageEditorHtml.vue new file mode 100644 index 00000000000..bce1497ac72 --- /dev/null +++ b/client/galaxy/scripts/components/PageEditor/PageEditorHtml.vue @@ -0,0 +1,682 @@ + + + + + diff --git a/client/galaxy/scripts/components/PageEditor/index.js b/client/galaxy/scripts/components/PageEditor/index.js new file mode 100644 index 00000000000..326ba8fc57f --- /dev/null +++ b/client/galaxy/scripts/components/PageEditor/index.js @@ -0,0 +1 @@ +export { default as PageEditor } from "./PageEditor.vue"; diff --git a/client/galaxy/scripts/components/PageEditor/util.js b/client/galaxy/scripts/components/PageEditor/util.js new file mode 100644 index 00000000000..5339f5172f6 --- /dev/null +++ b/client/galaxy/scripts/components/PageEditor/util.js @@ -0,0 +1,24 @@ +import $ from "jquery"; +import _l from "utils/localization"; +import { getAppRoot } from "onload/loadConfig"; +import { show_modal, hide_modal } from "layout/modal"; + +export const save = (pageId, content) => { + console.log(`content is ${content}`); + + show_modal(_l("Saving page"), _l("progress")); + + // Do save. + $.ajax({ + url: `${getAppRoot()}page/save`, + type: "POST", + data: { + id: pageId, + content: content, + _: "true" + }, + success: function() { + hide_modal(); + } + }); +}; diff --git a/client/galaxy/scripts/components/PluginList.vue b/client/galaxy/scripts/components/PluginList.vue index ae55e131fa5..7acf1ff0533 100644 --- a/client/galaxy/scripts/components/PluginList.vue +++ b/client/galaxy/scripts/components/PluginList.vue @@ -73,9 +73,9 @@ export default { }; }, created() { - let Galaxy = getGalaxyInstance(); + const Galaxy = getGalaxyInstance(); let url = `${getAppRoot()}api/plugins`; - let dataset_id = Galaxy.params.dataset_id; + const dataset_id = Galaxy.params.dataset_id; if (dataset_id) { this.fixed = true; this.selected = dataset_id; @@ -95,8 +95,8 @@ export default { if (this.fixed) { this.create(plugin); } else { - let Galaxy = getGalaxyInstance(); - let history_id = Galaxy.currHistoryPanel && Galaxy.currHistoryPanel.model.id; + const Galaxy = getGalaxyInstance(); + const history_id = Galaxy.currHistoryPanel && Galaxy.currHistoryPanel.model.id; if (history_id) { axios .get(`${getAppRoot()}api/plugins/${plugin.name}?history_id=${history_id}`) @@ -116,7 +116,7 @@ export default { } }, create: function(plugin) { - let href = `${plugin.href}?dataset_id=${this.selected}`; + const href = `${plugin.href}?dataset_id=${this.selected}`; if (plugin.target == "_top") { window.location.href = href; } else { @@ -131,7 +131,7 @@ export default { ); }, _errorMessage: function(e) { - let message = e && e.response && e.response.data && e.response.data.err_msg; + const message = e && e.response && e.response.data && e.response.data.err_msg; return message || "Request failed for an unknown reason."; } } diff --git a/client/galaxy/scripts/components/RuleCollectionBuilder.vue b/client/galaxy/scripts/components/RuleCollectionBuilder.vue index dccaac7e71a..fccb9d37f3b 100644 --- a/client/galaxy/scripts/components/RuleCollectionBuilder.vue +++ b/client/galaxy/scripts/components/RuleCollectionBuilder.vue @@ -670,7 +670,7 @@ const ColumnSelector = { return colHeaders; } const remaining = {}; - for (let key in colHeaders) { + for (const key in colHeaders) { if (this.target.indexOf(parseInt(key)) === -1) { remaining[key] = colHeaders[key]; } @@ -685,7 +685,7 @@ const ColumnSelector = { handleInput(value) { if (this.multiple) { // https://stackoverflow.com/questions/262427/why-does-parseint-yield-nan-with-arraymap - let val = value.map(idx => parseInt(idx)); + const val = value.map(idx => parseInt(idx)); this.$emit("update:target", val); } else { let val = parseInt(value); @@ -951,7 +951,7 @@ export default { if (this.initialElements !== null) { const collectionType = this.initialElements.collection_type; const collectionTypeRanks = collectionType.split(":"); - for (let index in collectionTypeRanks) { + for (const index in collectionTypeRanks) { rules.push({ type: "add_column_metadata", value: "identifier" + index @@ -1187,7 +1187,7 @@ export default { }, mappedTargets() { const targets = []; - for (let mapping of this.mapping) { + for (const mapping of this.mapping) { targets.push(mapping.type); } return targets; @@ -1195,7 +1195,7 @@ export default { unmappedTargets() { const targets = []; const mappedTargets = this.mappedTargets; - for (let target in MAPPING_TARGETS) { + for (const target in MAPPING_TARGETS) { const targetModes = MAPPING_TARGETS[target].modes; if (targetModes && targetModes.indexOf(this.elementsType) < 0) { @@ -1217,16 +1217,16 @@ export default { return targets; }, colHeaders() { - let { data, columns } = this.hotData; + const { data, columns } = this.hotData; return RuleDefs.colHeadersFor(data, columns); }, colHeadersDisplay() { const formattedHeaders = []; - for (let colIndex in this.colHeaders) { + for (const colIndex in this.colHeaders) { const colHeader = this.colHeaders[colIndex]; formattedHeaders[colIndex] = `${_.escape(colHeader)}`; const mappingDisplay = []; - for (let mapping of this.mapping) { + for (const mapping of this.mapping) { if (mapping.columns.indexOf(parseInt(colIndex)) !== -1) { const mappingDef = MAPPING_TARGETS[mapping.type]; mappingDisplay.push(`${_.escape(mappingDef.columnHeader || mappingDef.label)}`); @@ -1245,7 +1245,7 @@ export default { }, mappingAsDict() { const asDict = {}; - for (let mapping of this.mapping) { + for (const mapping of this.mapping) { asDict[mapping.type] = mapping; } return asDict; @@ -1261,7 +1261,7 @@ export default { collectionType = "list:list:list:paired"; } const collectionTypeRanks = collectionType.split(":"); - for (let index in collectionTypeRanks) { + for (const index in collectionTypeRanks) { const collectionTypeRank = collectionTypeRanks[index]; if (collectionTypeRank == "list") { // TODO: drop the numeral at the end if only flat list @@ -1323,7 +1323,7 @@ export default { if (requiresSourceColumn && !mappingAsDict.ftp_path && !mappingAsDict.url) { valid = false; } - for (var rule of this.rules) { + for (const rule of this.rules) { if (rule.error) { valid = false; } @@ -1487,7 +1487,7 @@ export default { this.mapping.splice(index, 1); }, refreshAndWait(response) { - let Galaxy = getGalaxyInstance(); + const Galaxy = getGalaxyInstance(); if (Galaxy && Galaxy.currHistoryPanel) { Galaxy.currHistoryPanel.refreshContents(); } @@ -1506,7 +1506,7 @@ export default { "Unknown error encountered while running your upload job, this could be a server issue or a problem with the upload definition."; this.doFullJobCheck(jobId); } else { - let Galaxy = getGalaxyInstance(); + const Galaxy = getGalaxyInstance(); const history = Galaxy && Galaxy.currHistoryPanel && Galaxy.currHistoryPanel.model; history.refresh(); this.oncreate(); @@ -1592,13 +1592,13 @@ export default { this.oncreate(); } } else { - let Galaxy = getGalaxyInstance(); + const Galaxy = getGalaxyInstance(); const historyId = Galaxy.currHistoryPanel.model.id; let elements, targets; if (collectionType) { targets = []; const elementsByCollectionName = this.creationElementsForFetch(); - for (let collectionName in elementsByCollectionName) { + for (const collectionName in elementsByCollectionName) { const target = { destination: { type: "hdca" }, elements: elementsByCollectionName[collectionName], @@ -1656,11 +1656,11 @@ export default { const collectionType = this.collectionType; const elementsByName = {}; - let dataByCollection = {}; + const dataByCollection = {}; const collectionNameMap = this.mappingAsDict.collection_name; if (collectionNameMap) { const collectionNameTarget = collectionNameMap.columns[0]; - for (let dataIndex in data) { + for (const dataIndex in data) { const row = data[dataIndex]; const name = row[collectionNameTarget]; if (!dataByCollection[name]) { @@ -1673,11 +1673,11 @@ export default { dataByCollection[this.collectionName] = data; } - for (let collectionName in dataByCollection) { + for (const collectionName in dataByCollection) { const elements = []; const identifiers = []; - for (let dataIndex in dataByCollection[collectionName]) { + for (const dataIndex in dataByCollection[collectionName]) { const rowData = data[dataIndex]; // For each row, find place in depth for this element. @@ -1724,7 +1724,7 @@ export default { .slice(1) .join(":"); let found = false; - for (let element of elementsAtDepth) { + for (const element of elementsAtDepth) { if (element["name"] == identifier) { elementsAtDepth = element[subElementProp]; identifiersAtDepth = identifiersAtDepth[identifier]; @@ -1795,7 +1795,7 @@ export default { const datasets = []; - for (let dataIndex in data) { + for (const dataIndex in data) { const res = this._datasetFor(dataIndex, data, mappingAsDict); datasets.push(res); } @@ -1806,7 +1806,7 @@ export default { const parentIdentifiers = parentIdentifiers_ ? parentIdentifiers_ : []; let data = []; let sources = []; - for (let element of elements) { + for (const element of elements) { const elementObject = element.object; const identifiers = parentIdentifiers.concat([element.element_identifier]); const collectionTypeLevelSepIndex = collectionType.indexOf(":"); @@ -1818,7 +1818,7 @@ export default { sources.push(source); } else { const restCollectionType = collectionType.slice(collectionTypeLevelSepIndex + 1); - let elementObj = this.populateElementsFromCollectionDescription( + const elementObj = this.populateElementsFromCollectionDescription( elementObject.elements, restCollectionType, identifiers @@ -1892,14 +1892,14 @@ export default { const tags = []; if (mappingAsDict.tags) { const tagColumns = mappingAsDict.tags.columns; - for (var tagColumn of tagColumns) { + for (const tagColumn of tagColumns) { const tag = data[dataIndex][tagColumn]; tags.push(tag); } } if (mappingAsDict.group_tags) { const groupTagColumns = mappingAsDict.group_tags.columns; - for (var groupTagColumn of groupTagColumns) { + for (const groupTagColumn of groupTagColumns) { const tag = data[dataIndex][groupTagColumn]; tags.push("group:" + tag); } @@ -1919,7 +1919,7 @@ export default { if (this.elementsType !== "collection_contents") { let columnCount = null; if (this.elementsType == "datasets") { - for (let element of this.initialElements) { + for (const element of this.initialElements) { if (element.history_content_type == "dataset_collection") { this.errorMessage = "This component can only be used with datasets, you have specified one or more collections."; @@ -1927,7 +1927,7 @@ export default { } } } else { - for (let row of this.initialElements) { + for (const row of this.initialElements) { if (columnCount == null) { columnCount = row.length; } else { diff --git a/client/galaxy/scripts/components/Select2.js b/client/galaxy/scripts/components/Select2.js index 639511faeaf..66629b348b8 100644 --- a/client/galaxy/scripts/components/Select2.js +++ b/client/galaxy/scripts/components/Select2.js @@ -6,7 +6,7 @@ export default { props: ["options", "value", "placeholder"], template: ``, mounted: function() { - var vm = this; + const vm = this; $(this.$el) // init select2 .select2({ data: this.options, placeholder: this.placeholder, allowClear: this.placeholder }) diff --git a/client/galaxy/scripts/components/Sharing.vue b/client/galaxy/scripts/components/Sharing.vue index 002c90a3f6e..84ffbde9e3c 100644 --- a/client/galaxy/scripts/components/Sharing.vue +++ b/client/galaxy/scripts/components/Sharing.vue @@ -25,7 +25,9 @@ - + Edit Share Url

@@ -170,13 +172,14 @@ export default { return this.item.published ? "accessible via link and published" : "accessible via link"; }, item_url() { - return `${window.location.protocol}//${window.location.hostname}:${window.location.port}${getAppRoot()}${ + const port = window.location.port ? `:${window.location.port}` : ""; + return `${window.location.protocol}//${window.location.hostname}${port}${getAppRoot()}${ this.item.username_and_slug }`; }, item_url_parts() { - let str = this.item_url; - let index = str.lastIndexOf("/"); + const str = this.item_url; + const index = str.lastIndexOf("/"); return [str.substring(0, index + 1), str.substring(index + 1)]; }, published_url() { @@ -196,7 +199,7 @@ export default { } }, data() { - let Galaxy = getGalaxyInstance(); + const Galaxy = getGalaxyInstance(); return { ready: false, has_username: Galaxy.user.get("username"), @@ -232,7 +235,7 @@ export default { .catch(error => (this.err_msg = error.response.data.err_msg)); }, setUsername: function() { - let Galaxy = getGalaxyInstance(); + const Galaxy = getGalaxyInstance(); axios .put(`${getAppRoot()}api/users/${Galaxy.user.id}/information/inputs`, { username: this.new_username || "" @@ -245,7 +248,7 @@ export default { .catch(error => (this.err_msg = error.response.data.err_msg)); }, setSharing: function(action, user_id) { - let data = { + const data = { action: action, user_id: user_id }; @@ -263,7 +266,7 @@ export default { .catch(error => (this.err_msg = error.response.data.err_msg)); }, createSlugHandler: function() { - var on_start = function(text_elt) { + const on_start = function(text_elt) { // Replace URL with URL text. $("#item-url").hide(); $("#item-url-text").show(); @@ -279,14 +282,14 @@ export default { ); }); }; - var on_finish = function(text_elt) { + const on_finish = function(text_elt) { // Replace URL text with URL. $("#item-url-text").hide(); $("#item-url").show(); // Set URL to new value. - var new_url = $("#item-url-text").text(); - var item_url_obj = $("#item-url"); + const new_url = $("#item-url-text").text(); + const item_url_obj = $("#item-url"); item_url_obj.attr("href", new_url); item_url_obj.text(new_url); }; diff --git a/client/galaxy/scripts/components/Tags/StatelessTags.vue b/client/galaxy/scripts/components/Tags/StatelessTags.vue index 8cbaac11041..c45ab737cdd 100644 --- a/client/galaxy/scripts/components/Tags/StatelessTags.vue +++ b/client/galaxy/scripts/components/Tags/StatelessTags.vue @@ -44,7 +44,7 @@ export default { }, data() { // initialize toggle value - let isClosed = this.useToggleLink && this.value.length > this.maxVisibleTags; + const isClosed = this.useToggleLink && this.value.length > this.maxVisibleTags; return { tagText: "", @@ -92,13 +92,13 @@ export default { }, beforeAddingTag($event) { if (!this.emitHookEvent("before-adding-tag", $event)) { - let { tag, addTag } = $event; + const { tag, addTag } = $event; addTag(tag); } }, beforeDeletingTag($event) { if (!this.emitHookEvent("before-deleting-tag", $event)) { - let { tag, deleteTag } = $event; + const { tag, deleteTag } = $event; deleteTag(tag); } }, diff --git a/client/galaxy/scripts/components/Tags/backboneTagService.js b/client/galaxy/scripts/components/Tags/backboneTagService.js index 7b162b5fd1c..60912fa3d62 100644 --- a/client/galaxy/scripts/components/Tags/backboneTagService.js +++ b/client/galaxy/scripts/components/Tags/backboneTagService.js @@ -12,7 +12,7 @@ export class BackboneTagService extends TagService { } async save(rawTag) { - let tag = createTag(rawTag); + const tag = createTag(rawTag); if (!tag.valid) { throw new Error("Invalid tag"); } @@ -27,7 +27,7 @@ export class BackboneTagService extends TagService { } async delete(rawTag) { - let tag = createTag(rawTag); + const tag = createTag(rawTag); let tags = new Set(this.model.attributes.tags); tags.delete(tag.text); diff --git a/client/galaxy/scripts/components/Tags/model.js b/client/galaxy/scripts/components/Tags/model.js index 2449e22ce0e..a9dd7c68eae 100644 --- a/client/galaxy/scripts/components/Tags/model.js +++ b/client/galaxy/scripts/components/Tags/model.js @@ -21,9 +21,9 @@ function TagModel(props = {}) { enumerable: true, get: function() { if (this.text.startsWith("name:")) { - let { primary, contrasting, darker } = keyedColorScheme(this.text); + const { primary, contrasting, darker } = keyedColorScheme(this.text); - let styles = { + const styles = { "background-color": primary, color: contrasting, "border-color": darker @@ -82,7 +82,7 @@ export function createTag(data) { // Returns tags in "newTags" that aren't present in "existingTags" export const diffTags = (newTags, existingTags) => { - let newModels = newTags.map(createTag); - let existingModels = existingTags.map(createTag); + const newModels = newTags.map(createTag); + const existingModels = existingTags.map(createTag); return newModels.filter(tag => !existingModels.some(st => st.equals(tag))); }; diff --git a/client/galaxy/scripts/components/Tags/mounts.js b/client/galaxy/scripts/components/Tags/mounts.js index ae502d87e9b..2ac287cdcc4 100644 --- a/client/galaxy/scripts/components/Tags/mounts.js +++ b/client/galaxy/scripts/components/Tags/mounts.js @@ -14,17 +14,17 @@ import { BackboneTagService } from "./backboneTagService"; * by the tagging_common.mako file */ export const mountMakoTags = (options = {}, el) => { - let { id, itemClass, tags = [], disabled = false, context = "unspecified" } = options; + const { id, itemClass, tags = [], disabled = false, context = "unspecified" } = options; - let propData = { + const propData = { storeKey: `${itemClass}-${id}`, tagService: new TagService({ id, itemClass, context }), tags, disabled }; - let fn = mountVueComponent(Tags); - let vm = fn(propData, el); + const fn = mountVueComponent(Tags); + const vm = fn(propData, el); vm.$on("tag-click", makoClickHandler(options, vm)); return vm; }; @@ -40,15 +40,13 @@ const makoClickHandler = (options, vm) => return; } - let { tagClickFn = "none", clickUrl } = options; + const { tagClickFn = "none", clickUrl } = options; switch (tagClickFn) { - // I made this match the existing behavior, but I am not clear on - // the reason why this link redirects to a raw json page case "community_tag_click": if (undefined !== clickUrl) { - let suffix = tag.value ? `:${tag.value}` : ""; - let href = `${clickUrl}?f-tags=${tag.text}${suffix}`; + const suffix = tag.value ? `:${tag.value}` : ""; + const href = `${clickUrl}?f-tags=${tag.text}${suffix}`; redirectToUrl(href); } break; @@ -63,22 +61,22 @@ const makoClickHandler = (options, vm) => * Mount function when a backbone model is provided. */ export const mountModelTags = (options = {}, el) => { - let { model, disabled = false, context = "unspecified" } = options; + const { model, disabled = false, context = "unspecified" } = options; if (!model) { console.warn("Missing model in mountModelTags"); return; } - let { id, model_class: itemClass, tags = [] } = model.attributes; + const { id, model_class: itemClass, tags = [] } = model.attributes; - let propData = { + const propData = { storeKey: `${itemClass}-${id}`, tagService: new BackboneTagService({ id, itemClass, context, model }), tags, disabled }; - let fn = mountVueComponent(Tags); + const fn = mountVueComponent(Tags); return fn(propData, el); }; diff --git a/client/galaxy/scripts/components/Tags/tagService.js b/client/galaxy/scripts/components/Tags/tagService.js index 9a0e6f36228..496c0d4726e 100644 --- a/client/galaxy/scripts/components/Tags/tagService.js +++ b/client/galaxy/scripts/components/Tags/tagService.js @@ -52,13 +52,13 @@ export class TagService { * @returns Promise yielding new tag model */ async save(rawTag) { - let { id, itemClass, context } = this; - let tag = createTag(rawTag); + const { id, itemClass, context } = this; + const tag = createTag(rawTag); if (!tag.valid) { throw new Error("Invalid tag"); } - let url = `/tag/add_tag_async?item_id=${id}&item_class=${itemClass}&context=${context}&new_tag=${tag.text}`; - let response = await axios.get(url); + const url = `/tag/add_tag_async?item_id=${id}&item_class=${itemClass}&context=${context}&new_tag=${tag.text}`; + const response = await axios.get(url); if (response.status !== 200) { throw new Error(`Unable to save tag: ${tag}`); } @@ -71,10 +71,12 @@ export class TagService { * @returns Promise yielding deleted tag model */ async delete(rawTag) { - let { id, itemClass, context } = this; - let tag = createTag(rawTag); - let url = `/tag/remove_tag_async?item_id=${id}&item_class=${itemClass}&context=${context}&tag_name=${tag.text}`; - let response = await axios.get(url); + const { id, itemClass, context } = this; + const tag = createTag(rawTag); + const url = `/tag/remove_tag_async?item_id=${id}&item_class=${itemClass}&context=${context}&tag_name=${ + tag.text + }`; + const response = await axios.get(url); if (response.status !== 200) { throw new Error(`Unable to delete tag: ${tag}`); } @@ -87,9 +89,9 @@ export class TagService { * @returns Promise yielding an array of tag models */ async autocomplete(searchText) { - let { id, itemClass } = this; - let url = `/tag/tag_autocomplete_data?item_id=${id}&item_class=${itemClass}&q=${searchText}`; - let response = await axios.get(url); + const { id, itemClass } = this; + const url = `/tag/tag_autocomplete_data?item_id=${id}&item_class=${itemClass}&q=${searchText}`; + const response = await axios.get(url); if (response.status !== 200) { throw new Error(`Unable to retrieve autocomplete tags for search string: ${searchText}`); } diff --git a/client/galaxy/scripts/components/Tags/tagService.test.js b/client/galaxy/scripts/components/Tags/tagService.test.js index 82169efd590..ee4b3d77c2f 100644 --- a/client/galaxy/scripts/components/Tags/tagService.test.js +++ b/client/galaxy/scripts/components/Tags/tagService.test.js @@ -132,7 +132,7 @@ describe("Tags/tagService.js", () => { ); // spam a bunch of key inputs - for (var i = 0; i < spamCount; i++) svc.autocompleteSearchText = new String(Math.random()); + for (let i = 0; i < spamCount; i++) svc.autocompleteSearchText = new String(Math.random()); svc.autocompleteSearchText = searchString; }); }); diff --git a/client/galaxy/scripts/components/Toolshed/Categories.vue b/client/galaxy/scripts/components/Toolshed/Categories.vue new file mode 100644 index 00000000000..fce86dabae7 --- /dev/null +++ b/client/galaxy/scripts/components/Toolshed/Categories.vue @@ -0,0 +1,67 @@ + + diff --git a/client/galaxy/scripts/components/Toolshed/Index.vue b/client/galaxy/scripts/components/Toolshed/Index.vue new file mode 100644 index 00000000000..c5788beb546 --- /dev/null +++ b/client/galaxy/scripts/components/Toolshed/Index.vue @@ -0,0 +1,118 @@ + + diff --git a/client/galaxy/scripts/components/Toolshed/InstallationButton.vue b/client/galaxy/scripts/components/Toolshed/InstallationButton.vue new file mode 100644 index 00000000000..8176cd0360f --- /dev/null +++ b/client/galaxy/scripts/components/Toolshed/InstallationButton.vue @@ -0,0 +1,45 @@ + + diff --git a/client/galaxy/scripts/components/Toolshed/InstallationSettings.vue b/client/galaxy/scripts/components/Toolshed/InstallationSettings.vue new file mode 100644 index 00000000000..41673819f71 --- /dev/null +++ b/client/galaxy/scripts/components/Toolshed/InstallationSettings.vue @@ -0,0 +1,138 @@ + + diff --git a/client/galaxy/scripts/components/Toolshed/Repositories.vue b/client/galaxy/scripts/components/Toolshed/Repositories.vue new file mode 100644 index 00000000000..f70d611ea4d --- /dev/null +++ b/client/galaxy/scripts/components/Toolshed/Repositories.vue @@ -0,0 +1,102 @@ + + diff --git a/client/galaxy/scripts/components/Toolshed/RepositoryDetails.vue b/client/galaxy/scripts/components/Toolshed/RepositoryDetails.vue new file mode 100644 index 00000000000..a79d9216d5e --- /dev/null +++ b/client/galaxy/scripts/components/Toolshed/RepositoryDetails.vue @@ -0,0 +1,177 @@ + + diff --git a/client/galaxy/scripts/components/Toolshed/RepositoryTools.vue b/client/galaxy/scripts/components/Toolshed/RepositoryTools.vue new file mode 100644 index 00000000000..3db30cb03ca --- /dev/null +++ b/client/galaxy/scripts/components/Toolshed/RepositoryTools.vue @@ -0,0 +1,67 @@ + + diff --git a/client/galaxy/scripts/components/Toolshed/ServerSelection.vue b/client/galaxy/scripts/components/Toolshed/ServerSelection.vue new file mode 100644 index 00000000000..abd24b8d522 --- /dev/null +++ b/client/galaxy/scripts/components/Toolshed/ServerSelection.vue @@ -0,0 +1,40 @@ + + diff --git a/client/galaxy/scripts/components/Toolshed/services.js b/client/galaxy/scripts/components/Toolshed/services.js new file mode 100644 index 00000000000..a1a7551e459 --- /dev/null +++ b/client/galaxy/scripts/components/Toolshed/services.js @@ -0,0 +1,110 @@ +import axios from "axios"; +import { getAppRoot } from "onload/loadConfig"; + +/** Request repositories, categories etc from toolshed server **/ +export class Services { + async getCategories(toolshedUrl) { + const paramString = `tool_shed_url=${toolshedUrl}&controller=categories`; + const url = `${getAppRoot()}api/tool_shed/request?${paramString}`; + try { + const response = await axios.get(url); + return response.data; + } catch (e) { + return this._errorMessage(e); + } + } + async getRepositories(params) { + params["controller"] = "repositories"; + const paramString = this._getParamString(params); + const url = `${getAppRoot()}api/tool_shed/request?${paramString}`; + try { + const response = await axios.get(url); + const data = response.data; + const incoming = data.hits.map(x => x.repository); + incoming.forEach(x => { + x.times_downloaded = this._formatCount(x.times_downloaded); + x.repository_url = `${data.hostname}repository?repository_id=${x.id}`; + }); + return incoming; + } catch (e) { + return this._errorMessage(e); + } + } + async getDetails(toolshedUrl, repository_id) { + const paramString = `tool_shed_url=${toolshedUrl}&id=${repository_id}&controller=repositories&action=metadata`; + const url = `${getAppRoot()}api/tool_shed/request?${paramString}`; + try { + const response = await axios.get(url); + const data = response.data; + const table = Object.keys(data).map(key => data[key]); + table.sort((a, b) => b.numeric_revision - a.numeric_revision); + table.forEach(x => { + if (Array.isArray(x.tools)) { + x.profile = x.tools.reduce( + (value, current) => (current.profile > value ? current.profile : value), + null + ); + } + }); + return table; + } catch (e) { + return `${this._errorMessage(e)}, ${url}`; + } + } + async getInstalledRepositories(repo) { + const paramsString = `name=${repo.name}&owner=${repo.repo_owner_username}`; + const url = `${getAppRoot()}api/tool_shed_repositories?${paramsString}`; + try { + const response = await axios.get(url); + const data = response.data; + const result = {}; + data.forEach(x => { + const d = { + status: x.status, + installed: !x.deleted && !x.uninstalled + }; + result[x.changeset_revision] = result[x.installed_changeset_revision] = d; + }); + return result; + } catch (e) { + this._errorMessage(e); + } + } + async installRepository(payload) { + const url = `${getAppRoot()}api/tool_shed_repositories`; + try { + const response = await axios.post(url, payload); + return response.data; + } catch (e) { + return this._errorMessage(e); + } + } + async uninstallRepository(params) { + const paramsString = Object.keys(params).reduce(function(previous, key) { + return `${previous}${key}=${params[key]}&`; + }, ""); + const url = `${getAppRoot()}api/tool_shed_repositories?${paramsString}`; + try { + const response = await axios.delete(url); + return response.data; + } catch (e) { + return this._errorMessage(e); + } + } + _formatCount(value) { + if (value > 1000) return `>${Math.floor(value / 1000)}k`; + return value; + } + _errorMessage(e) { + let message = "Request failed."; + if (e.response) { + message = e.response.data.err_msg || `${e.response.statusText} (${e.response.status})`; + } + return message; + } + _getParamString(params) { + return Object.keys(params).reduce(function(previous, key) { + return `${previous}${key}=${params[key]}&`; + }, ""); + } +} diff --git a/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.test.js b/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.test.js new file mode 100644 index 00000000000..7d930f4e2e8 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.test.js @@ -0,0 +1,87 @@ +import sinon from "sinon"; +import flushPromises from "flush-promises"; +import { shallowMount, createLocalVue } from "@vue/test-utils"; + +import { default as CloudAuth, __RewireAPI__ as rewire } from "./CloudAuth"; +import CloudAuthItem from "./CloudAuthItem"; +import { Credential } from "./model"; + +import _l from "utils/localization"; +import BootstrapVue from "bootstrap-vue"; + +// test data +import listCredentials from "./testdata/listCredentials.json"; + +const localVue = createLocalVue(); +localVue.use(BootstrapVue); +localVue.filter("localize", value => _l(value)); + +describe("CloudAuth component", () => { + let stub, wrapper; + + let mockSvc = { + listCredentials: async () => null + }; + + rewire.__Rewire__("svc", mockSvc); + + beforeEach(async () => { + let creds = listCredentials.map(Credential.create); + stub = sinon.stub(mockSvc, "listCredentials").resolves(creds); + wrapper = shallowMount(CloudAuth, { localVue }); + await flushPromises(); + }); + + afterEach(() => { + if (stub) { + stub.restore(); + } + }); + + describe("initialization", () => { + it("should render the initial list", () => { + assert(wrapper); + assert(wrapper.contains(CloudAuthItem)); + assert(wrapper.vm.items.length == 2); + assert(wrapper.vm.filteredItems.length == 2); + }); + }); + + describe("text filter", () => { + it("should show filter result by text match", () => { + let results; + + wrapper.vm.filter = "aws"; + results = wrapper.vm.filteredItems; + assert(wrapper.contains(CloudAuthItem)); + assert(results.length == 1, `Wrong number of items: ${results.length}`); + + wrapper.vm.filter = "azure"; + results = wrapper.vm.filteredItems; + assert(results.length == 1, `Wrong number of items: ${results.length}`); + + wrapper.vm.filter = ""; + results = wrapper.vm.filteredItems; + assert(results.length == 2, `Wrong number of items: ${results.length}`); + }); + }); + + describe("create button", () => { + it("clicking create button should add a blank key", () => { + let results = wrapper.vm.filteredItems; + assert(wrapper.contains(CloudAuthItem)); + assert(results.length == 2, `Wrong number of items: ${results.length}`); + + let button = wrapper.find("button[name=createNewKey]"); + assert(button); + button.trigger("click"); + + results = wrapper.vm.filteredItems; + assert(results.length == 3, `Wrong number of items: ${results.length}`); + + let blank = results.find(i => i.id == null); + assert(blank, "missing blank key"); + assert(blank.id == null); + }); + }); +}); diff --git a/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.vue b/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.vue new file mode 100644 index 00000000000..74bc81e5592 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/CloudAuth.vue @@ -0,0 +1,378 @@ + + + + + diff --git a/client/galaxy/scripts/components/User/CloudAuth/CloudAuthItem.vue b/client/galaxy/scripts/components/User/CloudAuth/CloudAuthItem.vue new file mode 100644 index 00000000000..39d8c43f7d5 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/CloudAuthItem.vue @@ -0,0 +1,95 @@ + + + diff --git a/client/galaxy/scripts/components/User/CloudAuth/CredentialConfig.vue b/client/galaxy/scripts/components/User/CloudAuth/CredentialConfig.vue new file mode 100644 index 00000000000..8314cb93bb8 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/CredentialConfig.vue @@ -0,0 +1,44 @@ + + + diff --git a/client/galaxy/scripts/components/User/CloudAuth/CredentialForm.vue b/client/galaxy/scripts/components/User/CloudAuth/CredentialForm.vue new file mode 100644 index 00000000000..0dd4d16a332 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/CredentialForm.vue @@ -0,0 +1,108 @@ + + + diff --git a/client/galaxy/scripts/components/User/CloudAuth/index.js b/client/galaxy/scripts/components/User/CloudAuth/index.js new file mode 100644 index 00000000000..3e5bd9184df --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/index.js @@ -0,0 +1 @@ +export { default as CloudAuth } from "./CloudAuth.vue"; diff --git a/client/galaxy/scripts/components/User/CloudAuth/model/AwsConfig.js b/client/galaxy/scripts/components/User/CloudAuth/model/AwsConfig.js new file mode 100644 index 00000000000..269d75578a3 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/model/AwsConfig.js @@ -0,0 +1,27 @@ +import { safeAssign } from "utils/safeAssign"; +import { BaseModel } from "./BaseModel"; + +export class AwsConfig extends BaseModel { + constructor(props = {}) { + super(); + this.role_arn = ""; + safeAssign(this, props); + this.updateState(); + } +} + +AwsConfig.setValidator(function(model) { + const errors = {}; + if (!model.role_arn.length) { + errors.role_arn = "Missing role_arn"; + } + return errors; +}); + +AwsConfig.fields = { + role_arn: { + label: "Role ARN", + description: "The Amazon resource name (ARN) of the role to be assumed by Galaxy.", + placeholder: "arn:aws:iam::XXXXXXXXXXXX:role/XXXXXXXXXXX" + } +}; diff --git a/client/galaxy/scripts/components/User/CloudAuth/model/AwsConfig.test.js b/client/galaxy/scripts/components/User/CloudAuth/model/AwsConfig.test.js new file mode 100644 index 00000000000..462683198e2 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/model/AwsConfig.test.js @@ -0,0 +1,22 @@ +import { AwsConfig } from "./AwsConfig"; + +describe("AwsConfig", () => { + it("should instantiate", () => { + let instance = new AwsConfig(); + assert(instance); + assert(!instance.dirty); + assert(!instance.valid); + }); + + it("should validate role_arn", () => { + let instance = new AwsConfig(); + instance.role_arn = "abc"; + assert(instance.fieldValid("role_arn")); + }); + + it("should invalidate role_arn", () => { + let instance = new AwsConfig(); + instance.role_arn = ""; + assert(!instance.fieldValid("role_arn")); + }); +}); diff --git a/client/galaxy/scripts/components/User/CloudAuth/model/AzureConfig.js b/client/galaxy/scripts/components/User/CloudAuth/model/AzureConfig.js new file mode 100644 index 00000000000..7cee6dacf06 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/model/AzureConfig.js @@ -0,0 +1,61 @@ +import { safeAssign } from "utils/safeAssign"; +import { BaseModel } from "./BaseModel"; + +export class AzureConfig extends BaseModel { + constructor(props = {}) { + super(); + this.tenant_id = ""; + this.client_id = ""; + this.client_secret = ""; + safeAssign(this, props); + this.updateState(); + } +} + +AzureConfig.setValidator(function(model) { + const errors = {}; + + if (model.tenant_id.length < 36) { + errors.tenant_id = "Tenant ID too short"; + } + if (!model.tenant_id) { + errors.tenant_id = "Missing Tenant ID"; + } + + if (model.client_id.length < 36) { + errors.client_id = "Client ID too short"; + } + if (!model.client_id) { + errors.client_id = "Missing client ID"; + } + + if (!model.client_secret) { + errors.client_secret = "Missing secret"; + } + + return errors; +}); + +AzureConfig.fields = { + tenant_id: { + label: "Tenant ID", + mask: "********-****-****-****-************", + placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + maxlength: 36, + description: "Your Tenant ID (or Directory ID) on Azure." + }, + client_id: { + label: "Client ID", + mask: "********-****-****-****-************", + placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + maxlength: 36, + description: "The Client ID (or Application ID) you defined for Galaxy on your Azure directory." + }, + client_secret: { + label: "Client Secret", + mask: "", + placeholder: "Client Secret", + description: + "A secret string you obtained from Azure portal that Galaxy can use to prove its identity when requesting tokens to access your resources." + } +}; diff --git a/client/galaxy/scripts/components/User/CloudAuth/model/AzureConfig.test.js b/client/galaxy/scripts/components/User/CloudAuth/model/AzureConfig.test.js new file mode 100644 index 00000000000..cc90a205bd6 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/model/AzureConfig.test.js @@ -0,0 +1,58 @@ +import { AzureConfig } from "./AzureConfig"; + +describe("AzureConfig", () => { + it("should instantiate", () => { + let instance = new AzureConfig(); + assert(instance); + assert(!instance.dirty); + assert(!instance.valid); + }); + + describe("client_secret", () => { + it("should validate client_secret", () => { + let instance = new AzureConfig(); + instance.client_secret = "abc"; + assert(instance.fieldValid("client_secret")); + }); + + it("should invalidate client_secret", () => { + let instance = new AzureConfig(); + instance.client_secret = ""; + assert(!instance.fieldValid("client_secret")); + }); + }); + + describe("tenant_id", () => { + it("should validate tenant_id", () => { + let validTenantId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; + let instance = new AzureConfig(); + instance.tenant_id = validTenantId; + assert(instance.fieldValid("tenant_id")); + }); + + it("should invalidate tenant_id", () => { + let instance = new AzureConfig(); + instance.tenant_id = ""; + assert(!instance.fieldValid("tenant_id")); + instance.tenant_id = "asdfa"; + assert(!instance.fieldValid("tenant_id")); + }); + }); + + describe("client_id", () => { + it("should validate client_id", () => { + let validClientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; + let instance = new AzureConfig(); + instance.client_id = validClientId; + assert(instance.fieldValid("client_id")); + }); + + it("should invalidate client_id", () => { + let instance = new AzureConfig(); + instance.client_id = ""; + assert(!instance.fieldValid("client_id")); + instance.client_id = "asdf"; + assert(!instance.fieldValid("client_id")); + }); + }); +}); diff --git a/client/galaxy/scripts/components/User/CloudAuth/model/BaseModel.js b/client/galaxy/scripts/components/User/CloudAuth/model/BaseModel.js new file mode 100644 index 00000000000..651ce0d0ab1 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/model/BaseModel.js @@ -0,0 +1,102 @@ +const lastState = new WeakMap(); +const validators = new WeakMap(); +const transients = new WeakMap(); +const counter = new WeakMap(); + +// Helps with Vue for-loop keys +let instanceCounter = 0; + +export class BaseModel { + constructor() { + counter.set(this, instanceCounter++); + } + + /* object ID */ + + get counter() { + return counter.get(this); + } + + /* Dirty state tracking */ + + get dirty() { + return lastState.get(this) != this.state; + } + + get clean() { + return !this.dirty; + } + + get state() { + // build state JSON ignoring transient fields + const tFields = this.transientFields; + return JSON.stringify(this, function(key) { + if (!tFields.has(key)) { + return this[key]; + } + return undefined; + }); + } + + get lastState() { + return lastState.get(this); + } + + get transientFields() { + const key = this.constructor; + if (!transients.has(key)) { + transients.set(key, new Set()); + } + return transients.get(key); + } + + updateState() { + lastState.set(this, this.state); + } + + // Setting a property name as transient for a class means its + // state will be caclulated without regard to that propety + static setTransient(...fieldNames) { + const klass = this; // this will be a class + if (!transients.has(klass)) { + transients.set(klass, new Set()); + } + + const fields = transients.get(klass); + fieldNames.forEach(fieldName => fields.add(fieldName)); + transients.set(klass, fields); + } + + /* Validation */ + + get valid() { + return Object.keys(this.validationErrors).length == 0; + } + + get validationErrors() { + return this.constructor.validate(this); + } + + errorMessage(field) { + if (field in this.validationErrors) { + return this.validationErrors[field]; + } + return "errmessage"; + } + + fieldValid(fieldName) { + return !(fieldName in this.validationErrors); + } + + static validate(model) { + if (validators.has(this)) { + const validator = validators.get(this); + return validator(model); + } + throw new Error("Missing validator"); + } + + static setValidator(validationFunction) { + validators.set(this, validationFunction); + } +} diff --git a/client/galaxy/scripts/components/User/CloudAuth/model/Credential.js b/client/galaxy/scripts/components/User/CloudAuth/model/Credential.js new file mode 100644 index 00000000000..defa15e67df --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/model/Credential.js @@ -0,0 +1,104 @@ +import { safeAssign } from "utils/safeAssign"; +import { ResourceProviders } from "./ResourceProviders"; +import { BaseModel } from "./BaseModel"; + +export class Credential extends BaseModel { + constructor(props = {}) { + super(); + + this.id = null; + this.description = ""; + this.authn_id = null; // identity provider + this.provider = null; // resource provider + + // transient props, exclude from state + this.expanded = false; + this.loading = false; + + // populate props + const options = Object.assign({}, Credential.defaults, props); + safeAssign(this, options); + + // init nested config + this.config = new this.configClass(options.config); + + // initialize state + this.updateState(); + } + + get title() { + return this.description.length ? this.description : this.provider; + } + + get valid() { + return super.valid && this.config.valid; + } + + // Alias for provider also changes config object when updated + // Set this when updating in the UI + + get resourceProvider() { + return this.provider; + } + + set resourceProvider(newProvider) { + this.provider = newProvider; + this.config = new this.configClass({}); + } + + // Polymorphic config class + + get configClass() { + return ResourceProviders.get(this.provider).klass; + } + + // Methods + + match(searchText = "") { + // TODO: more robust object matching? + return searchText.length ? this.title.includes(searchText) : true; + } + + // Statics + + static get defaults() { + return { + authn_id: null, + provider: "aws", + expanded: false + }; + } + + static create(props = {}) { + return new Credential(props); + } +} + +/** + * Transient fields do not factor into the state + * for purposes of dirty tracking + */ +Credential.setTransient("expanded", "loading"); + +/** + * A validator function's job is to return an object + * where the keys are field names and the values are + * error messages. + */ +Credential.setValidator(function(model) { + const errors = {}; + + if (!model.provider) { + errors.provider = "Provider must be set"; + } + + if (!model.authn_id) { + errors.authn_id = "Please pick an identity provider"; + } + + if (!model.config.valid) { + errors.config = "Invalid config object"; + } + + return errors; +}); diff --git a/client/galaxy/scripts/components/User/CloudAuth/model/Credential.test.js b/client/galaxy/scripts/components/User/CloudAuth/model/Credential.test.js new file mode 100644 index 00000000000..ee2be51b315 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/model/Credential.test.js @@ -0,0 +1,106 @@ +import { Credential } from "./Credential"; + +describe("Credential model", () => { + describe("basic model props", () => { + it("should exist", () => { + assert(Credential); + }); + + it("should build with defaults", () => { + let instance = new Credential(); + assert(instance); + }); + + it("should build with props", () => { + let description = "i am the test description"; + let props = { description }; + let instance = new Credential(props); + assert(instance); + assert(instance.description == description); + }); + + it("default config object should be AWS", () => { + let instance = new Credential(); + assert("role_arn" in instance.config, "Missing role_arn prop"); + assert(instance.config.constructor.name == "AwsConfig"); + }); + + it("should load a different config object given the right provider", () => { + let provider = "azure"; + let props = { provider }; + let instance = new Credential(props); + assert(instance.provider == provider); + assert(instance.config.constructor.name == "AzureConfig"); + }); + + it("should dynamically switch config objects as resourceProvider is changed", () => { + let instance = new Credential(); + assert(instance.provider == "aws", "Should default to AWS config' "); + instance.resourceProvider = "azure"; + assert(instance.config.constructor.name == "AzureConfig", "Should have been AzureConfig"); + instance.resourceProvider = "aws"; + assert(instance.config.constructor.name == "AwsConfig", "Should be AWS Config again"); + }); + }); + + describe("dirty state", () => { + it("should flag as dirty when a prop is changed", () => { + let instance = new Credential(); + instance.description = "foo"; + assert(instance.dirty); + }); + + it("should flag as clean when a prop is restored", () => { + let instance = new Credential(); + instance.description = "foo"; + assert(instance.dirty); + instance.description = ""; + assert(!instance.dirty); + }); + + it("should not flag as dirty when a transient prop is changed", () => { + let instance = new Credential(); + instance.loading = true; + assert(!instance.dirty); + }); + }); + + describe("validation", () => { + it("a new object should be invalid", () => { + let instance = new Credential(); + assert(!instance.valid); + }); + + it("it should become valid when props are assigned", () => { + let instance = new Credential(); + instance.authn_id = "floob"; + instance.resourceProvider = "aws"; + instance.config.role_arn = "abc"; + assert(instance.valid); + }); + + it("it should be valid when initialized with correct props", () => { + let props = { + authn_id: "asdfasdf", + provider: "aws", + config: { + role_arn: "floobar" + } + }; + let instance = new Credential(props); + assert(instance.valid); + }); + + it("should be invalid when bad props assigned", () => { + let props = { + authn_id: "asdfasdf", + provider: "aws", + config: { + role_arn: "" + } + }; + let instance = new Credential(props); + assert(!instance.valid); + }); + }); +}); diff --git a/client/galaxy/scripts/components/User/CloudAuth/model/IdentityProvider.js b/client/galaxy/scripts/components/User/CloudAuth/model/IdentityProvider.js new file mode 100644 index 00000000000..7c0974fc50b --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/model/IdentityProvider.js @@ -0,0 +1,29 @@ +import { safeAssign } from "utils/safeAssign"; + +export class IdentityProvider { + constructor(props = {}) { + this.id = ""; + this.provider = ""; + safeAssign(this, props); + } + + // Aliases + + get authn_id() { + return this.id; + } + + get text() { + return this.provider; + } + + get value() { + return this.id; + } + + // Statics + + static create(props = {}) { + return new IdentityProvider(props); + } +} diff --git a/client/galaxy/scripts/components/User/CloudAuth/model/ResourceProviders.js b/client/galaxy/scripts/components/User/CloudAuth/model/ResourceProviders.js new file mode 100644 index 00000000000..965e254b2f1 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/model/ResourceProviders.js @@ -0,0 +1,14 @@ +import { AwsConfig } from "./AwsConfig"; +import { AzureConfig } from "./AzureConfig"; + +export const ResourceProviders = new Map(); + +ResourceProviders.set("aws", { + klass: AwsConfig, + label: "Amazon Web Services (AWS)" +}); + +ResourceProviders.set("azure", { + klass: AzureConfig, + label: "Microsoft Azure" +}); diff --git a/client/galaxy/scripts/components/User/CloudAuth/model/index.js b/client/galaxy/scripts/components/User/CloudAuth/model/index.js new file mode 100644 index 00000000000..24f019b67c5 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/model/index.js @@ -0,0 +1,3 @@ +export { Credential } from "./Credential"; +export { IdentityProvider } from "./IdentityProvider"; +export { ResourceProviders } from "./ResourceProviders"; diff --git a/client/galaxy/scripts/components/User/CloudAuth/model/service.js b/client/galaxy/scripts/components/User/CloudAuth/model/service.js new file mode 100644 index 00000000000..28e6339b8c0 --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/model/service.js @@ -0,0 +1,78 @@ +/** + * Data retrieval/storage for the auth keys + */ + +import axios from "axios"; +import { Credential, IdentityProvider } from "./index"; +import { getRootFromIndexLink } from "onload"; + +const getUrl = path => getRootFromIndexLink() + path; + +export async function listCredentials() { + const url = getUrl("api/cloud/authz"); + const response = await axios.get(url); + if (response.status != 200) { + throw new Error("Unexpected response from listing."); + } + return response.data.map(Credential.create); +} + +export async function getCredential(id) { + const url = getUrl("api/cloud/authz/${id}"); + const response = await axios.get(url); + if (response.status != 200) { + throw new Error("Unexpected response loading key."); + } + return Credential.create(response.data); +} + +export async function saveCredential(newItem) { + const model = Credential.create(newItem); + const response = await saveOrUpdate(model); + if (response.status != 200) { + throw new Error("Save failure."); + } + return Credential.create(response.data); +} + +async function saveOrUpdate(model) { + return model.id + ? axios.put(getUrl(`api/cloud/authz/${model.id}`), model) + : axios.post(getUrl("api/cloud/authz"), model); +} + +export async function deleteCredential(doomed) { + const model = Credential.create(doomed); + if (model.id) { + const url = getUrl(`api/cloud/authz/${doomed.id}`); + const response = await axios.delete(url); + if (response.status != 200) { + throw new Error("Delete failure."); + } + } + return model; +} + +// Memoize results (basically never changes) + +let identityProviders; + +export async function getIdentityProviders() { + if (!identityProviders) { + const url = getUrl("authnz"); + const response = await axios.get(url); + if (response.status != 200) { + throw new Error("Unable to load identity providers"); + } + identityProviders = response.data.map(IdentityProvider.create); + } + return identityProviders; +} + +export default { + listCredentials, + getCredential, + saveCredential, + deleteCredential, + getIdentityProviders +}; diff --git a/client/galaxy/scripts/components/User/CloudAuth/testdata/listCredentials.json b/client/galaxy/scripts/components/User/CloudAuth/testdata/listCredentials.json new file mode 100644 index 00000000000..82973dc3a7f --- /dev/null +++ b/client/galaxy/scripts/components/User/CloudAuth/testdata/listCredentials.json @@ -0,0 +1,32 @@ +[ + { + "authn_id": "f2db41e1fa331b3e", + "user_id": "f2db41e1fa331b3e", + "description": "aws description", + "last_update": "2019-04-04 09:13:16.135032", + "last_activity": "2019-04-04 09:13:16.135042", + "create_time": "2019-04-04 16:13:16.135755", + "provider": "aws", + "model_class": "CloudAuthz", + "config": { + "role_arn": "floobadooba" + }, + "id": "a7db2fac67043c7e" + }, + { + "authn_id": "f2db41e1fa331b3e", + "user_id": "f2db41e1fa331b3e", + "description": "azure description", + "last_update": "2019-04-03 20:21:51.350748", + "last_activity": "2019-04-03 20:21:51.350804", + "create_time": "2019-04-04 03:21:51.351508", + "provider": "azure", + "model_class": "CloudAuthz", + "config": { + "client_secret": "dannon likes Nickleback", + "client_id": "22222222-2222-2222-2222-222222222222", + "tenant_id": "11111111-1111-1111-1111-111111111111" + }, + "id": "b472e2eb553fa0d1" + } +] \ No newline at end of file diff --git a/client/galaxy/scripts/components/WorkflowImport.vue b/client/galaxy/scripts/components/WorkflowImport.vue index c202ebd5c2b..bd621e881e4 100644 --- a/client/galaxy/scripts/components/WorkflowImport.vue +++ b/client/galaxy/scripts/components/WorkflowImport.vue @@ -31,7 +31,7 @@ Vue.use(BootstrapVue); export default { data() { - let Galaxy = getGalaxyInstance(); + const Galaxy = getGalaxyInstance(); return { sourceFile: null, sourceURL: null, @@ -52,7 +52,7 @@ export default { if (!this.sourceFile && !this.sourceURL) { this.errorMessage = "You must provide a workflow archive URL or file."; } else { - let formData = new FormData(); + const formData = new FormData(); formData.append("archive_file", this.sourceFile); formData.append("archive_source", this.sourceURL); axios @@ -63,7 +63,7 @@ export default { }&status=success`; }) .catch(error => { - let message = error.response.data && error.response.data.err_msg; + const message = error.response.data && error.response.data.err_msg; this.errorMessage = message || "Import failed for an unknown reason."; }); } diff --git a/client/galaxy/scripts/components/admin/AdminServices.js b/client/galaxy/scripts/components/admin/AdminServices.js index 22c0c1c2e33..64701d52816 100644 --- a/client/galaxy/scripts/components/admin/AdminServices.js +++ b/client/galaxy/scripts/components/admin/AdminServices.js @@ -2,16 +2,16 @@ import { getAppRoot } from "onload/loadConfig"; import axios from "axios"; export function getErrorStack() { - let url = `${getAppRoot()}api/tools/error_stack`; + const url = `${getAppRoot()}api/tools/error_stack`; return axios.get(url); } export function getDisplayApplications() { - let url = `${getAppRoot()}api/display_applications`; + const url = `${getAppRoot()}api/display_applications`; return axios.get(url); } export function reloadDisplayApplications(ids) { - let url = `${getAppRoot()}api/display_applications/reload`; + const url = `${getAppRoot()}api/display_applications/reload`; return axios.post(url, { ids: ids }); } diff --git a/client/galaxy/scripts/components/admin/DataManager/DataManager.vue b/client/galaxy/scripts/components/admin/DataManager/DataManager.vue index 52251caaa16..5f60232b00f 100644 --- a/client/galaxy/scripts/components/admin/DataManager/DataManager.vue +++ b/client/galaxy/scripts/components/admin/DataManager/DataManager.vue @@ -6,18 +6,10 @@

None installed

- You do not currently have any Data Managers installed -
- - Data Managers can be installed from a - + You do not currently have any Data Managers installed.
- - Choose your data managing option from below. You may install additional Data Managers from a - - @@ -37,7 +29,7 @@ - + @@ -65,7 +57,7 @@ - + - ToolShed - ` -}; - export default { components: { - Alert, - ToolShedButton + Alert }, data() { return { diff --git a/client/galaxy/scripts/components/admin/DataManager/DataManagerJobs.vue b/client/galaxy/scripts/components/admin/DataManager/DataManagerJobs.vue index 87bf41802de..156e3208c0d 100644 --- a/client/galaxy/scripts/components/admin/DataManager/DataManagerJobs.vue +++ b/client/galaxy/scripts/components/admin/DataManager/DataManagerJobs.vue @@ -133,7 +133,7 @@ export default { ]; }, tableFields() { - let tableFields = this.fields.slice(0); + const tableFields = this.fields.slice(0); if (this.showCommandLine) { tableFields.splice(5, 0, { key: "commandLine", @@ -143,9 +143,9 @@ export default { return tableFields; }, tableItems() { - let tableItems = this.jobs.slice(0); + const tableItems = this.jobs.slice(0); - for (let item of tableItems) { + for (const item of tableItems) { // Nicer time formatting item["updateTime"] = item["updateTime"].replace("T", "\n"); diff --git a/client/galaxy/scripts/components/admin/DataManager/DataManagerView.vue b/client/galaxy/scripts/components/admin/DataManager/DataManagerView.vue index a49800d81d3..55cbf0ed008 100644 --- a/client/galaxy/scripts/components/admin/DataManager/DataManagerView.vue +++ b/client/galaxy/scripts/components/admin/DataManager/DataManagerView.vue @@ -1,11 +1,26 @@ + diff --git a/client/galaxy/scripts/components/admin/DataTypes.vue b/client/galaxy/scripts/components/admin/DataTypes.vue index 263139c02b2..e4e84f2b98c 100644 --- a/client/galaxy/scripts/components/admin/DataTypes.vue +++ b/client/galaxy/scripts/components/admin/DataTypes.vue @@ -56,20 +56,20 @@ export default { // Return predefined column headers merged // with all other column headers returned by the api columns() { - var columns = this.prettyColumns(); + let columns = this.prettyColumns(); // We have keys to look at if (this.keys.length > 0) { // Additional column headers only when option is selected if (this.showAllColumns) { - var keys = this.keys; + let keys = this.keys; // Filter out the predefined column headers - for (var c of columns) { + for (const c of columns) { keys = keys.filter(k => k !== c["dataIndex"]); } // Create column headers from each remaining key and merge // with predefined column headers columns = keys.reduce(function(m, k) { - var text = k[0].toUpperCase(); + let text = k[0].toUpperCase(); text += k.slice(1).replace(/_/g, " "); m.push({ text: text, diff --git a/client/galaxy/scripts/components/admin/DisplayApplications.vue b/client/galaxy/scripts/components/admin/DisplayApplications.vue index db9fe8e191f..3f751410fff 100644 --- a/client/galaxy/scripts/components/admin/DisplayApplications.vue +++ b/client/galaxy/scripts/components/admin/DisplayApplications.vue @@ -58,15 +58,15 @@ export default { }, computed: { applicationsIndex: function() { - let result = {}; - for (let app of this.applications) { + const result = {}; + for (const app of this.applications) { result[app.id] = app; } return result; }, applicationsAll: function() { - let result = []; - for (let app of this.applications) { + const result = []; + for (const app of this.applications) { result.push(app.id); } return result; @@ -99,8 +99,8 @@ export default { this._reload([id]); }, reloadAll: function() { - let ids = []; - for (let app of this.applications) { + const ids = []; + for (const app of this.applications) { ids.push(app.id); } this._reload(ids); @@ -119,15 +119,15 @@ export default { }); }, _highlightRows: function(appList, status) { - for (let appIndex of appList) { - let app = this.applicationsIndex[appIndex]; + for (const appIndex of appList) { + const app = this.applicationsIndex[appIndex]; if (app) { app._rowVariant = status; } } }, _errorMessage: function(e) { - let message = e && e.response && e.response.data && e.response.data.err_msg; + const message = e && e.response && e.response.data && e.response.data.err_msg; this.messageText = message || "Request failed for an unknown reason."; this.messageVariant = "danger"; } diff --git a/client/galaxy/scripts/components/admin/ErrorStack.vue b/client/galaxy/scripts/components/admin/ErrorStack.vue index b466213cf70..b83043c7caa 100644 --- a/client/galaxy/scripts/components/admin/ErrorStack.vue +++ b/client/galaxy/scripts/components/admin/ErrorStack.vue @@ -40,7 +40,7 @@ export default { this.errorStackLoaded = true; }) .catch(e => { - let message = e && e.response && e.response.data && e.response.data.err_msg; + const message = e && e.response && e.response.data && e.response.data.err_msg; this.messageText = message || "Request failed for an unknown reason."; }); } diff --git a/client/galaxy/scripts/components/admin/Jobs.vue b/client/galaxy/scripts/components/admin/Jobs.vue new file mode 100644 index 00000000000..f3500e08676 --- /dev/null +++ b/client/galaxy/scripts/components/admin/Jobs.vue @@ -0,0 +1,370 @@ + + + + + diff --git a/client/galaxy/scripts/components/login/Index.vue b/client/galaxy/scripts/components/login/Index.vue index aaa5a784f62..a166bdc0253 100644 --- a/client/galaxy/scripts/components/login/Index.vue +++ b/client/galaxy/scripts/components/login/Index.vue @@ -7,7 +7,7 @@
@@ -57,8 +57,8 @@ export default { type: String, required: false }, - smtp_server: { - type: String, + server_mail_configured: { + type: Boolean, required: false } } diff --git a/client/galaxy/scripts/components/login/Login.vue b/client/galaxy/scripts/components/login/Login.vue index e2c0ad2eb40..149251ea0c7 100644 --- a/client/galaxy/scripts/components/login/Login.vue +++ b/client/galaxy/scripts/components/login/Login.vue @@ -6,7 +6,7 @@ - + @@ -20,12 +20,19 @@ Don't have an account? - Register here. + + Register here. + + + Registration for this Galaxy instance is disabled. Please contact an administrator for + assistance. + - Sign in with {{ idp.charAt(0).toUpperCase() + idp.slice(1) }} + Sign in with + {{ idp.charAt(0).toUpperCase() + idp.slice(1) }}
@@ -55,14 +62,18 @@ export default { } }, data() { - let galaxy = getGalaxyInstance(); - let oidc_idps = galaxy.config.oidc; + const galaxy = getGalaxyInstance(); + const oidc_idps = galaxy.config.oidc; // Icons to use for each IdP - let oidc_idps_icons = {'google': 'fa fa-google'}; + const oidc_idps_icons = { google: "fa fa-google" }; // Add default icons to IdPs without icons - oidc_idps.filter(function(key) { return oidc_idps_icons[key] === undefined; }).forEach(function(idp) { - oidc_idps_icons[idp] = 'fa fa-id-card' - }); + oidc_idps + .filter(function(key) { + return oidc_idps_icons[key] === undefined; + }) + .forEach(function(idp) { + oidc_idps_icons[idp] = "fa fa-id-card"; + }); return { login: null, password: null, @@ -70,12 +81,12 @@ export default { provider: null, messageText: null, messageVariant: null, + allowUserCreation: galaxy.config.allow_user_creation, redirect: galaxy.params.redirect, session_csrf_token: galaxy.session_csrf_token, enable_oidc: galaxy.config.enable_oidc, oidc_idps: oidc_idps, oidc_idps_icons: oidc_idps_icons - }; }, computed: { @@ -90,7 +101,7 @@ export default { } }, submitGalaxyLogin: function(method) { - let rootUrl = getAppRoot(); + const rootUrl = getAppRoot(); axios .post(`${rootUrl}user/login`, this.$data) .then(response => { @@ -107,12 +118,12 @@ export default { }) .catch(error => { this.messageVariant = "danger"; - let message = error.response.data && error.response.data.err_msg; + const message = error.response.data && error.response.data.err_msg; this.messageText = message || "Login failed for an unknown reason."; }); }, submitOIDCLogin: function(idp) { - let rootUrl = getAppRoot(); + const rootUrl = getAppRoot(); axios .post(`${rootUrl}authnz/${idp}/login`) .then(response => { @@ -123,12 +134,12 @@ export default { }) .catch(error => { this.messageVariant = "danger"; - let message = error.response.data && error.response.data.err_msg; + const message = error.response.data && error.response.data.err_msg; this.messageText = message || "Login failed for an unknown reason."; }); }, reset: function(ev) { - let rootUrl = getAppRoot(); + const rootUrl = getAppRoot(); ev.preventDefault(); axios .post(`${rootUrl}user/reset_password`, { email: this.login }) @@ -138,7 +149,7 @@ export default { }) .catch(error => { this.messageVariant = "danger"; - let message = error.response.data && error.response.data.err_msg; + const message = error.response.data && error.response.data.err_msg; this.messageText = message || "Password reset failed for an unknown reason."; }); } diff --git a/client/galaxy/scripts/components/login/Password.vue b/client/galaxy/scripts/components/login/Password.vue index 9d25aa38ffd..f50b6a6afb1 100644 --- a/client/galaxy/scripts/components/login/Password.vue +++ b/client/galaxy/scripts/components/login/Password.vue @@ -22,7 +22,7 @@ Vue.use(BootstrapVue); export default { data() { - let Galaxy = getGalaxyInstance(); + const Galaxy = getGalaxyInstance(); return { token: Galaxy.params.token, user: Galaxy.params.expired_user, @@ -40,7 +40,7 @@ export default { }, methods: { submit: function(ev) { - let urlRoot = getAppRoot(); + const urlRoot = getAppRoot(); ev.preventDefault(); axios .post(`${urlRoot}user/change_password`, { @@ -55,7 +55,7 @@ export default { }) .catch(error => { this.messageVariant = "danger"; - let message = error.response.data && error.response.data.err_msg; + const message = error.response.data && error.response.data.err_msg; this.messageText = message || "Password change failed for an unknown reason."; }); } diff --git a/client/galaxy/scripts/components/login/Register.vue b/client/galaxy/scripts/components/login/Register.vue index 2009c783a1f..588e2251ec9 100644 --- a/client/galaxy/scripts/components/login/Register.vue +++ b/client/galaxy/scripts/components/login/Register.vue @@ -1,7 +1,7 @@