mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-10-25 00:12:08 +08:00
Merge branch 'dev' into activate_toolshed
This commit is contained in:
+11
-1
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
+223
-7
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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})`;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -29,12 +29,16 @@
|
||||
</template>
|
||||
<script>
|
||||
import _ from "underscore";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import axios from "axios";
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import * as bibtexParse from "libs/bibtexParse";
|
||||
import { convertLaTeX } from "latex-to-unicode-converter";
|
||||
import { stringifyLaTeX } from "latex-parser";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
props: {
|
||||
source: {
|
||||
@@ -71,17 +75,17 @@ export default {
|
||||
.get(`${getAppRoot()}api/${this.source}/${this.id}/citations`)
|
||||
.then(response => {
|
||||
this.content = "";
|
||||
for (var rawCitation of response.data) {
|
||||
for (const rawCitation of response.data) {
|
||||
try {
|
||||
var citation = {
|
||||
const citation = {
|
||||
fields: {},
|
||||
entryType: undefined
|
||||
};
|
||||
var parsed = bibtexParse.toJSON(rawCitation.content);
|
||||
let parsed = bibtexParse.toJSON(rawCitation.content);
|
||||
if (parsed) {
|
||||
parsed = _.first(parsed);
|
||||
citation.entryType = parsed.entryType || undefined;
|
||||
for (var key in parsed.entryTags) {
|
||||
for (const key in parsed.entryTags) {
|
||||
citation.fields[key.toLowerCase()] = parsed.entryTags[key];
|
||||
}
|
||||
}
|
||||
@@ -98,18 +102,18 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
formattedReference: function(citation) {
|
||||
var entryType = citation.entryType;
|
||||
var fields = citation.fields;
|
||||
const entryType = citation.entryType;
|
||||
const fields = citation.fields;
|
||||
|
||||
var ref = "";
|
||||
var authorsAndYear = `${this._asSentence(
|
||||
let ref = "";
|
||||
const authorsAndYear = `${this._asSentence(
|
||||
(fields.author ? fields.author : "") + (fields.year ? ` (${fields.year})` : "")
|
||||
)} `;
|
||||
var title = fields.title || "";
|
||||
var pages = fields.pages ? `pp. ${fields.pages}` : "";
|
||||
var address = fields.address;
|
||||
const title = fields.title || "";
|
||||
const pages = fields.pages ? `pp. ${fields.pages}` : "";
|
||||
const address = fields.address;
|
||||
if (entryType == "article") {
|
||||
var volume =
|
||||
const volume =
|
||||
(fields.volume ? fields.volume : "") +
|
||||
(fields.number ? ` (${fields.number})` : "") +
|
||||
(pages ? `, ${pages}` : "");
|
||||
@@ -144,19 +148,19 @@ export default {
|
||||
fields.howpublished
|
||||
)}${this._asSentence(fields.note)}`;
|
||||
}
|
||||
var doiUrl = "";
|
||||
let doiUrl = "";
|
||||
if (fields.doi) {
|
||||
doiUrl = `https://doi.org/${fields.doi}`;
|
||||
ref += `[<a href="${doiUrl}" target="_blank">doi:${fields.doi}</a>]`;
|
||||
}
|
||||
var url = fields.url || doiUrl;
|
||||
const url = fields.url || doiUrl;
|
||||
if (url) {
|
||||
ref += `[<a href="${url}" target="_blank">Link</a>]`;
|
||||
}
|
||||
return convertLaTeX({ onError: (error, latex) => `{${stringifyLaTeX(latex)}}` }, ref);
|
||||
},
|
||||
_formatBookInfo: function(fields) {
|
||||
var info = "";
|
||||
let info = "";
|
||||
if (fields.chapter) {
|
||||
info += `${fields.chapter} in `;
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("services.js/Services", () => {
|
||||
let items = services.getItems(rawData);
|
||||
expect(items.length).to.equals(1);
|
||||
let first = items[0];
|
||||
expect(first.name).to.equals("1: name_1");
|
||||
expect(first.label).to.equals("1: name_1");
|
||||
expect(first.download).to.equals("host/api/histories/0/contents/1/display");
|
||||
});
|
||||
});
|
||||
@@ -153,11 +153,9 @@ describe("DataDialog.vue", () => {
|
||||
emitted = wrapper.emitted();
|
||||
expect(wrapper.classes()).contain("data-dialog-modal");
|
||||
expect(wrapper.find(".fa-spinner").text()).to.equals("");
|
||||
expect(wrapper.find(".btn-secondary").text()).to.equals("Clear");
|
||||
expect(wrapper.find(".btn-primary").text()).to.equals("Ok");
|
||||
expect(wrapper.contains(".fa-spinner")).to.equals(true);
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(wrapper.findAll(".fa-copy").length).to.equals(2);
|
||||
expect(wrapper.findAll(".fa-folder").length).to.equals(2);
|
||||
expect(wrapper.findAll(".fa-file-o").length).to.equals(2);
|
||||
});
|
||||
});
|
||||
@@ -169,11 +167,9 @@ describe("DataDialog.vue", () => {
|
||||
emitted = wrapper.emitted();
|
||||
expect(wrapper.classes()).contain("data-dialog-modal");
|
||||
expect(wrapper.find(".fa-spinner").text()).to.equals("");
|
||||
expect(wrapper.find(".btn-secondary").text()).to.equals("Clear");
|
||||
expect(wrapper.find(".btn-primary").text()).to.equals("Ok");
|
||||
expect(wrapper.contains(".fa-spinner")).to.equals(true);
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(wrapper.findAll(".fa-copy").length).to.equals(2);
|
||||
expect(wrapper.findAll(".fa-folder").length).to.equals(2);
|
||||
expect(wrapper.findAll(".fa-file-o").length).to.equals(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,7 +20,14 @@
|
||||
<div class="fa fa-caret-left mr-1" />
|
||||
Back
|
||||
</b-btn>
|
||||
<b-btn size="sm" class="float-right ml-1" variant="primary" @click="finalize" :disabled="!hasValue">
|
||||
<b-btn
|
||||
v-if="multiple"
|
||||
size="sm"
|
||||
class="float-right ml-1"
|
||||
variant="primary"
|
||||
@click="finalize"
|
||||
:disabled="!hasValue"
|
||||
>
|
||||
Ok
|
||||
</b-btn>
|
||||
<b-btn size="sm" class="float-right" @click="modalShow = false"> Cancel </b-btn>
|
||||
@@ -99,7 +106,7 @@ export default {
|
||||
},
|
||||
/** Add highlighting for record variations, i.e. datasets vs. libraries/collections **/
|
||||
formatRows() {
|
||||
for (let item of this.items) {
|
||||
for (const item of this.items) {
|
||||
let _rowVariant = "active";
|
||||
if (item.isDataset) {
|
||||
_rowVariant = this.model.exists(item.id) ? "success" : "default";
|
||||
@@ -117,11 +124,13 @@ export default {
|
||||
} else {
|
||||
this.finalize();
|
||||
}
|
||||
} else {
|
||||
this.load(record.url);
|
||||
}
|
||||
},
|
||||
/** Called when selection is complete, values are formatted and parsed to external callback **/
|
||||
finalize: function() {
|
||||
let results = this.model.finalize();
|
||||
const results = this.model.finalize();
|
||||
this.modalShow = false;
|
||||
this.callback(results);
|
||||
},
|
||||
@@ -136,7 +145,7 @@ export default {
|
||||
.then(items => {
|
||||
if (this.library && this.urlTracker.atRoot()) {
|
||||
items.unshift({
|
||||
name: "Data Libraries",
|
||||
label: "Data Libraries",
|
||||
url: `${this.root}api/libraries`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<b-input-group>
|
||||
<b-input v-model="filter" placeholder="Type to Search" />
|
||||
<b-input-group-append>
|
||||
<b-btn :disabled="!filter" @click="filter = ''">Clear</b-btn>
|
||||
<b-btn :disabled="!filter" @click="filter = ''"><i class="fa fa-times"/></b-btn>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</template>
|
||||
@@ -21,7 +21,7 @@ export default {
|
||||
return this.value;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', val);
|
||||
this.$emit("input", val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
@row-clicked="clicked"
|
||||
@filtered="filtered"
|
||||
>
|
||||
<template slot="name" slot-scope="data">
|
||||
<i v-if="data.item.isDataset" class="fa fa-file-o" /> <i v-else class="fa fa-copy" />
|
||||
<template slot="label" slot-scope="data">
|
||||
<i v-if="data.item.isDataset" class="fa fa-file-o" /> <i v-else class="fa fa-folder" />
|
||||
{{ data.value ? data.value : "-" }}
|
||||
</template>
|
||||
<template slot="details" slot-scope="data">
|
||||
@@ -21,17 +21,6 @@
|
||||
<template slot="time" slot-scope="data">
|
||||
{{ data.value ? data.value : "-" }}
|
||||
</template>
|
||||
<template slot="arrow" slot-scope="data">
|
||||
<b-button
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="py-0"
|
||||
v-if="!data.item.isDataset"
|
||||
@click.stop="load(data.item.url)"
|
||||
>
|
||||
View
|
||||
</b-button>
|
||||
</template>
|
||||
</b-table>
|
||||
<div v-if="nItems === 0">
|
||||
<div v-if="filter">
|
||||
@@ -69,7 +58,7 @@ export default {
|
||||
return {
|
||||
currentPage: 1,
|
||||
fields: {
|
||||
name: {
|
||||
label: {
|
||||
sortable: true
|
||||
},
|
||||
details: {
|
||||
@@ -77,11 +66,6 @@ export default {
|
||||
},
|
||||
time: {
|
||||
sortable: true
|
||||
},
|
||||
arrow: {
|
||||
label: "",
|
||||
sortable: false,
|
||||
class: "text-right"
|
||||
}
|
||||
},
|
||||
nItems: 0,
|
||||
@@ -105,10 +89,6 @@ export default {
|
||||
/** Collects selected datasets in value array **/
|
||||
clicked: function(record) {
|
||||
this.$emit("clicked", record);
|
||||
},
|
||||
/** Performs server request to retrieve data records **/
|
||||
load: function(url) {
|
||||
this.$emit("load", url);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ export class Model {
|
||||
if (!this.multiple) {
|
||||
this.values = {};
|
||||
}
|
||||
let key = record && record.id;
|
||||
const key = record && record.id;
|
||||
if (key) {
|
||||
if (!this.values[key]) {
|
||||
this.values[key] = record;
|
||||
|
||||
@@ -12,7 +12,7 @@ export class Services {
|
||||
axios
|
||||
.get(url)
|
||||
.then(response => {
|
||||
let items = this.getItems(response.data);
|
||||
const items = this.getItems(response.data);
|
||||
resolve(items);
|
||||
})
|
||||
.catch(e => {
|
||||
@@ -27,10 +27,10 @@ export class Services {
|
||||
|
||||
/** Returns the formatted results **/
|
||||
getItems(data) {
|
||||
let items = [];
|
||||
let stack = [data];
|
||||
const items = [];
|
||||
const stack = [data];
|
||||
while (stack.length > 0) {
|
||||
let root = stack.pop();
|
||||
const root = stack.pop();
|
||||
if (Array.isArray(root)) {
|
||||
root.forEach(element => {
|
||||
stack.push(element);
|
||||
@@ -40,7 +40,7 @@ export class Services {
|
||||
} else if (root.object) {
|
||||
stack.push(root.object);
|
||||
} else {
|
||||
let record = this.getRecord(root);
|
||||
const record = this.getRecord(root);
|
||||
if (record) {
|
||||
items.push(record);
|
||||
}
|
||||
@@ -58,16 +58,16 @@ export class Services {
|
||||
record.time = record.time.substring(0, 16).replace("T", " ");
|
||||
}
|
||||
if (record.model_class == "Library") {
|
||||
record.label = record.name;
|
||||
record.url = `${this.root}api/libraries/${record.id}/contents`;
|
||||
return record;
|
||||
} else if (record.hid) {
|
||||
record.name = `${record.hid}: ${record.name}`;
|
||||
record.label = `${record.hid}: ${record.name}`;
|
||||
record.download = `${this.host}/api/histories/${record.history_id}/contents/${record.id}/display`;
|
||||
return record;
|
||||
} else if (record.type == "file") {
|
||||
if (record.name && record.name[0] === "/") {
|
||||
record.name = record.name.substring(1);
|
||||
}
|
||||
record.src = "ldda";
|
||||
record.label = record.name;
|
||||
record.download = `${this.host}${this.root}api/libraries/datasets/download/uncompressed?ld_ids=${
|
||||
record.id
|
||||
}`;
|
||||
|
||||
@@ -11,7 +11,7 @@ export class UrlTracker {
|
||||
this.navigation.push(url);
|
||||
} else {
|
||||
this.navigation.pop();
|
||||
let navigationLength = this.navigation.length;
|
||||
const navigationLength = this.navigation.length;
|
||||
if (navigationLength > 0) {
|
||||
url = this.navigation[navigationLength - 1];
|
||||
} else {
|
||||
|
||||
@@ -32,8 +32,8 @@ export default {
|
||||
};
|
||||
},
|
||||
created: function() {
|
||||
let historyId = this.id,
|
||||
url = "";
|
||||
const historyId = this.id;
|
||||
let url = "";
|
||||
if (historyId !== undefined && historyId !== "" && historyId !== null) {
|
||||
url = getAppRoot() + "history/display_structured?id=" + historyId;
|
||||
} else {
|
||||
@@ -56,13 +56,13 @@ export default {
|
||||
});
|
||||
},
|
||||
_updateHistoryData: function(response) {
|
||||
let historyItems = response.data;
|
||||
const historyItems = response.data;
|
||||
this.historyTemplate = historyItems.template;
|
||||
this.historyJSON = historyItems.history_json;
|
||||
},
|
||||
makeHistoryView: function(historyDict) {
|
||||
window.hdas = historyDict.map(hda => {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
const Galaxy = getGalaxyInstance();
|
||||
return new HDAListItemEdit.HDAListItemEdit({
|
||||
model: new HDAModel.HistoryDatasetAssociation(hda),
|
||||
el: $("#hda-" + hda.id),
|
||||
@@ -74,7 +74,7 @@ export default {
|
||||
// toggle the body section of each item in the structure
|
||||
$(function() {
|
||||
$(".workflow, .tool").each((index, element) => {
|
||||
let body = $(element).children(".body");
|
||||
const body = $(element).children(".body");
|
||||
$(element)
|
||||
.children(".header")
|
||||
.click(e => {
|
||||
|
||||
@@ -36,7 +36,7 @@ export default {
|
||||
if (!this.sourceFile && !this.sourceURL) {
|
||||
this.errorMessage = "You must provide a history 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
|
||||
@@ -47,7 +47,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.";
|
||||
});
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export default {
|
||||
};
|
||||
},
|
||||
created: function() {
|
||||
let url = getAppRoot() + "history/view/" + this.id;
|
||||
const url = getAppRoot() + "history/view/" + this.id;
|
||||
this.ajaxCall(url, this.updateHistoryView);
|
||||
},
|
||||
methods: {
|
||||
@@ -86,7 +86,7 @@ export default {
|
||||
},
|
||||
makeHistoryView: function(history) {
|
||||
$(function() {
|
||||
let options = {
|
||||
const options = {
|
||||
hasMasthead: history.use_panels ? "true" : "false",
|
||||
userIsOwner: history.user_is_owner ? "true" : "false",
|
||||
isCurrent: history.history_is_current ? "true" : "false",
|
||||
@@ -104,14 +104,14 @@ export default {
|
||||
});
|
||||
},
|
||||
showStructure: function() {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
let displayStructureInstance = Vue.extend(DisplayStructure),
|
||||
const Galaxy = getGalaxyInstance();
|
||||
const displayStructureInstance = Vue.extend(DisplayStructure),
|
||||
mountView = document.createElement("div");
|
||||
Galaxy.page.center.display(mountView);
|
||||
new displayStructureInstance({ propsData: { id: QueryStringParsing.get("id") } }).$mount(mountView);
|
||||
},
|
||||
switchHistory: function() {
|
||||
let url = getAppRoot() + "history/switch_to_history?hist_id=" + this.historyHistory["id"];
|
||||
const url = getAppRoot() + "history/switch_to_history?hist_id=" + this.historyHistory["id"];
|
||||
this.ajaxCall(url, this.reloadPage);
|
||||
},
|
||||
reloadPage: function() {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div v-if="metricsByPlugins">
|
||||
<h3 v-if="includeTitle">Job Metrics</h3>
|
||||
<div v-for="plugin in orderedPlugins" v-bind:key="plugin">
|
||||
<h4>{{ plugin }}</h4>
|
||||
<table class="tabletip info_data_table">
|
||||
<tbody>
|
||||
<tr v-for="(metricValue, metricTitle) in metricsByPlugins[plugin]" v-bind:key="metricTitle">
|
||||
<td>{{ metricTitle }}</td>
|
||||
<td>{{ metricValue }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapCacheActions } from "vuex-cache";
|
||||
import { mapGetters } from "vuex";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
jobId: {
|
||||
type: String
|
||||
},
|
||||
datasetId: {
|
||||
type: String
|
||||
},
|
||||
datasetType: {
|
||||
type: String,
|
||||
default: "hda"
|
||||
},
|
||||
includeTitle: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
created: function() {
|
||||
if (this.jobId) {
|
||||
this.fetchJobMetricsForJobId(this.jobId);
|
||||
} else {
|
||||
this.fetchJobMetricsForDatasetId({ datasetId: this.datasetId, datasetType: this.datasetType });
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["getJobMetricsByDatasetId", "getJobMetricsByJobId"]),
|
||||
jobMetrics: function() {
|
||||
if (this.jobId) {
|
||||
return this.getJobMetricsByJobId(this.jobId);
|
||||
} else {
|
||||
return this.getJobMetricsByDatasetId(this.datasetId, this.datasetType);
|
||||
}
|
||||
},
|
||||
metricsByPlugins: function() {
|
||||
const metricsByPlugins = {};
|
||||
const metrics = this.jobMetrics;
|
||||
metrics.forEach(metric => {
|
||||
if (!(metric.plugin in metricsByPlugins)) {
|
||||
metricsByPlugins[metric.plugin] = {};
|
||||
}
|
||||
const metricsForPlugin = metricsByPlugins[metric.plugin];
|
||||
metricsForPlugin[metric.title] = metric.value;
|
||||
});
|
||||
return metricsByPlugins;
|
||||
},
|
||||
orderedPlugins: function() {
|
||||
return Object.keys(this.metricsByPlugins).sort();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapCacheActions(["fetchJobMetricsForDatasetId", "fetchJobMetricsForJobId"])
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as JobMetrics } from "./JobMetrics";
|
||||
|
||||
// functions for mounting job metrics in non-Vue environments
|
||||
export { mountJobMetrics } from "./mount";
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="tool-parameters">
|
||||
<h3 v-if="includeTitle">Tool Parameters</h3>
|
||||
<table class="tabletip" id="tool-parameters">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Input Parameter</th>
|
||||
<th>Value</th>
|
||||
<th v-if="anyNotes">Note for rerun</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(parameter, index) in parameters" v-bind:key="index">
|
||||
<td v-bind:style="{ 'padding-left': `${(parameter.depth - 1) * 10}px` }">
|
||||
{{ parameter.text }}
|
||||
</td>
|
||||
<td v-if="Array.isArray(parameter.value)">
|
||||
<ul style="padding-inline-start: 25px;">
|
||||
<li v-for="(elVal, index) in parameter.value" v-bind:key="index">
|
||||
<span v-if="elVal.src == 'hda'">
|
||||
<a :href="appRoot() + 'datasets/' + elVal.id + '/show_params'">
|
||||
{{ elVal.hid }}: {{ elVal.name }}
|
||||
</a>
|
||||
</span>
|
||||
<span v-else> {{ elVal.hid }}: {{ elVal.name }} </span>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td v-else>
|
||||
{{ parameter.value }}
|
||||
</td>
|
||||
<td v-if="anyNotes">
|
||||
<em v-if="parameter.notes">{{ parameter.notes }}</em>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<b-alert :show="hasParameterErrors" variant="danger">
|
||||
One or more of your original parameters may no longer be valid or displayed properly.
|
||||
</b-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import axios from "axios";
|
||||
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
props: {
|
||||
jobId: {
|
||||
type: String
|
||||
},
|
||||
datasetId: {
|
||||
type: String
|
||||
},
|
||||
datasetType: {
|
||||
type: String,
|
||||
default: "hda"
|
||||
},
|
||||
includeTitle: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
parameters: [],
|
||||
hasParameterErrors: false
|
||||
};
|
||||
},
|
||||
created: function() {
|
||||
let url;
|
||||
if (this.jobId) {
|
||||
url = `${getAppRoot()}api/jobs/${this.jobId}/parameters_display`;
|
||||
} else {
|
||||
url = `${getAppRoot()}api/datasets/${this.datasetId}/parameters_display?hda_ldda=${this.datasetType}`;
|
||||
}
|
||||
this.ajaxCall(url);
|
||||
},
|
||||
computed: {
|
||||
anyNotes: function() {
|
||||
let hasNotes = false;
|
||||
this.parameters.forEach(parameter => {
|
||||
hasNotes = hasNotes || parameter.notes;
|
||||
});
|
||||
return hasNotes;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
appRoot: function() {
|
||||
return getAppRoot();
|
||||
},
|
||||
ajaxCall: function(url) {
|
||||
axios
|
||||
.get(url)
|
||||
.then(response => response.data)
|
||||
.then(data => {
|
||||
this.hasParameterErrors = data.has_parameter_errors;
|
||||
this.parameters = data.parameters;
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
table.info_data_table {
|
||||
table-layout: fixed;
|
||||
word-break: break-word;
|
||||
}
|
||||
table.info_data_table td:nth-child(1) {
|
||||
width: 25%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as JobParameters } from "./JobParameters";
|
||||
|
||||
// functions for mounting job parameters display in non-Vue environments
|
||||
export { mountJobParameters } from "./mount";
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<span>
|
||||
<div class="unified-panel-header" unselectable="on">
|
||||
<div class="unified-panel-header-inner">
|
||||
Page Editor: {{ title }}
|
||||
<a id="save-button" class="btn btn-secondary fa fa-save float-right" @click="saveContent"></a>
|
||||
</div>
|
||||
</div>
|
||||
<page-editor-html :page-id="pageId" :content="content" ref="contentEditor" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageEditorHtml from "./PageEditorHtml";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
pageId: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
content: {
|
||||
type: String
|
||||
},
|
||||
title: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
saveContent: function() {
|
||||
this.$refs.contentEditor.saveContent();
|
||||
}
|
||||
},
|
||||
components: {
|
||||
PageEditorHtml
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,682 @@
|
||||
<template>
|
||||
<div class="unified-panel-body">
|
||||
<textarea name="page_content" :val="content"></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import $ from "jquery";
|
||||
import _l from "utils/localization";
|
||||
|
||||
import GridView from "mvc/grid/grid-view";
|
||||
import { show_modal, hide_modal } from "layout/modal";
|
||||
import { make_popupmenu } from "ui/popupmenu";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { buildConfig } from "utils/genericConfig";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { save } from "./util";
|
||||
|
||||
// Built a generic config container for the properties that
|
||||
// are passed in from python then used (formerly) globally
|
||||
const configs = buildConfig();
|
||||
|
||||
// Turn these window variables into config props list_objects_url,
|
||||
// set_accessible_url, get_name_and_link_url, editor_base_path,
|
||||
// iframe_base_path, save_url, page_id, page_list_url
|
||||
function setPageConfigs(incoming = {}) {
|
||||
return Object.assign(configs, incoming);
|
||||
}
|
||||
|
||||
var WYMeditor = window.WYMeditor;
|
||||
|
||||
var editor = null;
|
||||
|
||||
var CONTROLS = {
|
||||
// Item types.
|
||||
ITEM_HISTORY: "item_history",
|
||||
ITEM_DATASET: "item_dataset",
|
||||
ITEM_WORKFLOW: "item_workflow",
|
||||
ITEM_PAGE: "item_page",
|
||||
ITEM_VISUALIZATION: "item_visualization",
|
||||
|
||||
// Link dialogs.
|
||||
DIALOG_HISTORY_LINK: "link_history",
|
||||
DIALOG_DATASET_LINK: "link_dataset",
|
||||
DIALOG_WORKFLOW_LINK: "link_workflow",
|
||||
DIALOG_PAGE_LINK: "link_page",
|
||||
DIALOG_VISUALIZATION_LINK: "link_visualization",
|
||||
|
||||
// Embed dialogs.
|
||||
DIALOG_EMBED_HISTORY: "embed_history",
|
||||
DIALOG_EMBED_DATASET: "embed_dataset",
|
||||
DIALOG_EMBED_WORKFLOW: "embed_workflow",
|
||||
DIALOG_EMBED_PAGE: "embed_page",
|
||||
DIALOG_EMBED_VISUALIZATION: "embed_visualization"
|
||||
};
|
||||
|
||||
// Based on the dialog type, return a dictionary of information about an item
|
||||
function get_item_info(dialog_type) {
|
||||
var item_singular;
|
||||
var item_plural;
|
||||
var item_controller;
|
||||
var item_class;
|
||||
switch (dialog_type) {
|
||||
case CONTROLS.ITEM_HISTORY:
|
||||
item_singular = "History";
|
||||
item_plural = "Histories";
|
||||
item_controller = "history";
|
||||
item_class = "History";
|
||||
break;
|
||||
case CONTROLS.ITEM_DATASET:
|
||||
item_singular = "Dataset";
|
||||
item_plural = "Datasets";
|
||||
item_controller = "dataset";
|
||||
item_class = "HistoryDatasetAssociation";
|
||||
break;
|
||||
case CONTROLS.ITEM_WORKFLOW:
|
||||
item_singular = "Workflow";
|
||||
item_plural = "Workflows";
|
||||
item_controller = "workflow";
|
||||
item_class = "StoredWorkflow";
|
||||
break;
|
||||
case CONTROLS.ITEM_PAGE:
|
||||
item_singular = "Page";
|
||||
item_plural = "Pages";
|
||||
item_controller = "page";
|
||||
item_class = "Page";
|
||||
break;
|
||||
case CONTROLS.ITEM_VISUALIZATION:
|
||||
item_singular = "Visualization";
|
||||
item_plural = "Visualizations";
|
||||
item_controller = "visualization";
|
||||
item_class = "Visualization";
|
||||
break;
|
||||
}
|
||||
|
||||
// Build ajax URL that lists items for selection.
|
||||
var item_list_action = `list_${item_plural.toLowerCase()}_for_selection`;
|
||||
var ajax_url = configs.list_objects_url.replace("LIST_ACTION", item_list_action);
|
||||
|
||||
// Set up and return dict.
|
||||
return {
|
||||
singular: item_singular,
|
||||
plural: item_plural,
|
||||
controller: item_controller,
|
||||
iclass: item_class,
|
||||
list_ajax_url: ajax_url
|
||||
};
|
||||
}
|
||||
|
||||
// Make an item importable.
|
||||
function make_item_importable(item_controller, item_id, item_type) {
|
||||
var ajax_url = configs.set_accessible_url.replace("ITEM_CONTROLLER", item_controller);
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: ajax_url,
|
||||
data: { id: item_id, accessible: "True" },
|
||||
error: function() {
|
||||
alert(`Making ${item_type} accessible failed`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Completely replace WYM's dialog handling
|
||||
WYMeditor.editor.prototype.dialog = function(dialogType, dialogFeatures, bodyHtml) {
|
||||
var wym = this;
|
||||
var sStamp = wym.uniqueStamp();
|
||||
var selected = wym.selected();
|
||||
|
||||
// Swap out URL attribute for id/name attribute in link creation to enable anchor creation in page.
|
||||
function set_link_id() {
|
||||
// When "set link id" link clicked, update UI.
|
||||
$("#set_link_id").click(function() {
|
||||
// Set label.
|
||||
$("#link_attribute_label").text("ID/Name");
|
||||
|
||||
// Set input elt class, value.
|
||||
var attribute_input = $(".wym_href");
|
||||
attribute_input.addClass("wym_id").removeClass("wym_href");
|
||||
if (selected) attribute_input.val($(selected).attr("id"));
|
||||
|
||||
// Remove link.
|
||||
$(this).remove();
|
||||
});
|
||||
}
|
||||
|
||||
// LINK DIALOG
|
||||
if (dialogType == WYMeditor.DIALOG_LINK) {
|
||||
if (selected) {
|
||||
$(wym._options.hrefSelector).val($(selected).attr(WYMeditor.HREF));
|
||||
$(wym._options.srcSelector).val($(selected).attr(WYMeditor.SRC));
|
||||
$(wym._options.titleSelector).val($(selected).attr(WYMeditor.TITLE));
|
||||
$(wym._options.altSelector).val($(selected).attr(WYMeditor.ALT));
|
||||
}
|
||||
|
||||
// Get current URL, title.
|
||||
var curURL;
|
||||
|
||||
var curTitle;
|
||||
if (selected) {
|
||||
curURL = $(selected).attr("href");
|
||||
if (curURL == undefined) curURL = "";
|
||||
curTitle = $(selected).attr("title");
|
||||
if (curTitle == undefined) curTitle = "";
|
||||
}
|
||||
show_modal(
|
||||
"Create Link",
|
||||
`<div><div><label id='link_attribute_label'>URL <span style='float: right; font-size: 90%'><a href='#' id='set_link_id'>Create in-page anchor</a></span></label><br><input type='text' class='wym_href' value='${curURL}' size='40' /></div><div><label>Title</label><br><input type='text' class='wym_title' value='${curTitle}' size='40' /></div><div>`,
|
||||
{
|
||||
"Make link": function() {
|
||||
// Get URL, name/title.
|
||||
var sUrl = $(wym._options.hrefSelector).val() || "";
|
||||
|
||||
var sId = $(".wym_id").val() || "";
|
||||
var sName = $(wym._options.titleSelector).val() || "";
|
||||
|
||||
if (sUrl || sId) {
|
||||
// Create link.
|
||||
wym._exec(WYMeditor.CREATE_LINK, sStamp);
|
||||
|
||||
// Set link attributes.
|
||||
var link = $(`a[href=${sStamp}]`, wym._doc.body);
|
||||
link.attr(WYMeditor.HREF, sUrl)
|
||||
.attr(WYMeditor.TITLE, sName)
|
||||
.attr("id", sId);
|
||||
|
||||
// If link's text is default (wym-...), change it to the title.
|
||||
if (link.text().indexOf("wym-") === 0) {
|
||||
link.text(sName);
|
||||
}
|
||||
}
|
||||
hide_modal();
|
||||
},
|
||||
Cancel: function() {
|
||||
hide_modal();
|
||||
}
|
||||
},
|
||||
{},
|
||||
set_link_id
|
||||
);
|
||||
}
|
||||
|
||||
// IMAGE DIALOG
|
||||
if (dialogType == WYMeditor.DIALOG_IMAGE) {
|
||||
if (wym._selected_image) {
|
||||
$(`${wym._options.dialogImageSelector} ${wym._options.srcSelector}`).val(
|
||||
$(wym._selected_image).attr(WYMeditor.SRC)
|
||||
);
|
||||
$(`${wym._options.dialogImageSelector} ${wym._options.titleSelector}`).val(
|
||||
$(wym._selected_image).attr(WYMeditor.TITLE)
|
||||
);
|
||||
$(`${wym._options.dialogImageSelector} ${wym._options.altSelector}`).val(
|
||||
$(wym._selected_image).attr(WYMeditor.ALT)
|
||||
);
|
||||
}
|
||||
show_modal(
|
||||
"Image",
|
||||
"<div class='row'>" +
|
||||
"<label>URL</label><br>" +
|
||||
"<input type='text' class='wym_src' value='' size='40' />" +
|
||||
"</div>" +
|
||||
"<div class='row'>" +
|
||||
"<label>Alt text</label><br>" +
|
||||
"<input type='text' class='wym_alt' value='' size='40' />" +
|
||||
"</div>" +
|
||||
"<div class='row'>" +
|
||||
"<label>Title</label><br>" +
|
||||
"<input type='text' class='wym_title' value='' size='40' />" +
|
||||
"</div>",
|
||||
{
|
||||
Insert: function() {
|
||||
var sUrl = $(wym._options.srcSelector).val();
|
||||
if (sUrl.length > 0) {
|
||||
wym._exec(WYMeditor.INSERT_IMAGE, sStamp);
|
||||
$(`img[src$=${sStamp}]`, wym._doc.body)
|
||||
.attr(WYMeditor.SRC, sUrl)
|
||||
.attr(WYMeditor.TITLE, $(wym._options.titleSelector).val())
|
||||
.attr(WYMeditor.ALT, $(wym._options.altSelector).val());
|
||||
}
|
||||
hide_modal();
|
||||
},
|
||||
Cancel: function() {
|
||||
hide_modal();
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// TABLE DIALOG
|
||||
if (dialogType == WYMeditor.DIALOG_TABLE) {
|
||||
show_modal(
|
||||
"Table",
|
||||
"<div class='row'>" +
|
||||
"<label>Caption</label><br>" +
|
||||
"<input type='text' class='wym_caption' value='' size='40' />" +
|
||||
"</div>" +
|
||||
"<div class='row'>" +
|
||||
"<label>Summary</label><br>" +
|
||||
"<input type='text' class='wym_summary' value='' size='40' />" +
|
||||
"</div>" +
|
||||
"<div class='row'>" +
|
||||
"<label>Number Of Rows<br></label>" +
|
||||
"<input type='text' class='wym_rows' value='3' size='3' />" +
|
||||
"</div>" +
|
||||
"<div class='row'>" +
|
||||
"<label>Number Of Cols<br></label>" +
|
||||
"<input type='text' class='wym_cols' value='2' size='3' />" +
|
||||
"</div>",
|
||||
{
|
||||
Insert: function() {
|
||||
var iRows = $(wym._options.rowsSelector).val();
|
||||
var iCols = $(wym._options.colsSelector).val();
|
||||
|
||||
if (iRows > 0 && iCols > 0) {
|
||||
var table = wym._doc.createElement(WYMeditor.TABLE);
|
||||
var newRow = null;
|
||||
|
||||
var sCaption = $(wym._options.captionSelector).val();
|
||||
|
||||
//we create the caption
|
||||
var newCaption = table.createCaption();
|
||||
newCaption.innerHTML = sCaption;
|
||||
|
||||
//we create the rows and cells
|
||||
for (var x = 0; x < iRows; x++) {
|
||||
newRow = table.insertRow(x);
|
||||
for (var y = 0; y < iCols; y++) {
|
||||
newRow.insertCell(y);
|
||||
}
|
||||
}
|
||||
|
||||
//set the summary attr
|
||||
$(table).attr("summary", $(wym._options.summarySelector).val());
|
||||
|
||||
//append the table after the selected container
|
||||
var node = $(wym.findUp(wym.container(), WYMeditor.MAIN_CONTAINERS)).get(0);
|
||||
if (!node || !node.parentNode) $(wym._doc.body).append(table);
|
||||
else $(node).after(table);
|
||||
}
|
||||
hide_modal();
|
||||
},
|
||||
Cancel: function() {
|
||||
hide_modal();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// INSERT "GALAXY ITEM" LINK DIALOG
|
||||
if (
|
||||
dialogType == CONTROLS.DIALOG_HISTORY_LINK ||
|
||||
dialogType == CONTROLS.DIALOG_DATASET_LINK ||
|
||||
dialogType == CONTROLS.DIALOG_WORKFLOW_LINK ||
|
||||
dialogType == CONTROLS.DIALOG_PAGE_LINK ||
|
||||
dialogType == CONTROLS.DIALOG_VISUALIZATION_LINK
|
||||
) {
|
||||
// Based on item type, set useful vars.
|
||||
var item_info;
|
||||
switch (dialogType) {
|
||||
case CONTROLS.DIALOG_HISTORY_LINK:
|
||||
item_info = get_item_info(CONTROLS.ITEM_HISTORY);
|
||||
break;
|
||||
case CONTROLS.DIALOG_DATASET_LINK:
|
||||
item_info = get_item_info(CONTROLS.ITEM_DATASET);
|
||||
break;
|
||||
case CONTROLS.DIALOG_WORKFLOW_LINK:
|
||||
item_info = get_item_info(CONTROLS.ITEM_WORKFLOW);
|
||||
break;
|
||||
case CONTROLS.DIALOG_PAGE_LINK:
|
||||
item_info = get_item_info(CONTROLS.ITEM_PAGE);
|
||||
break;
|
||||
case CONTROLS.DIALOG_VISUALIZATION_LINK:
|
||||
item_info = get_item_info(CONTROLS.ITEM_VISUALIZATION);
|
||||
break;
|
||||
}
|
||||
|
||||
var grid = new GridView({
|
||||
url_base: item_info.list_ajax_url,
|
||||
embedded: true
|
||||
});
|
||||
const Galaxy = getGalaxyInstance();
|
||||
Galaxy.modal.show({
|
||||
title: `Insert Link to ${item_info.singular}`,
|
||||
body: $("<div/>")
|
||||
.append(grid.$el)
|
||||
.append(
|
||||
$("<div/>")
|
||||
.append('<input id="make-importable" type="checkbox" checked/>')
|
||||
.append(
|
||||
`Make the selected ${item_info.plural.toLowerCase()} accessible so that they can viewed by everyone.`
|
||||
)
|
||||
),
|
||||
closing_events: true,
|
||||
buttons: {
|
||||
Insert: function() {
|
||||
// Make selected items accessible (importable) ?
|
||||
var make_importable = false;
|
||||
if ($("#make-importable:checked").val() != null) make_importable = true;
|
||||
|
||||
// Insert links to history for each checked item.
|
||||
grid.$("input[name=id]:checked").each(function() {
|
||||
var item_id = $(this).val();
|
||||
|
||||
// Make item importable?
|
||||
if (make_importable) make_item_importable(item_info.controller, item_id, item_info.singular);
|
||||
|
||||
// Insert link(s) to item(s). This is done by getting item info and then manipulating wym.
|
||||
var url_template = configs.get_name_and_link_url + item_id;
|
||||
var ajax_url = url_template.replace("ITEM_CONTROLLER", item_info.controller);
|
||||
$.getJSON(ajax_url, returned_item_info => {
|
||||
// Get link text.
|
||||
wym._exec(WYMeditor.CREATE_LINK, sStamp);
|
||||
var link_text = $(`a[href=${sStamp}]`, wym._doc.body).text();
|
||||
|
||||
// Insert link: need to do different actions depending on link text.
|
||||
if (
|
||||
link_text == "" || // Firefox.
|
||||
link_text == sStamp // Safari
|
||||
) {
|
||||
// User selected no text; create link from scratch and use default text.
|
||||
wym.insert(
|
||||
`<a href='${returned_item_info.link}'>${item_info.singular} '${
|
||||
returned_item_info.name
|
||||
}'</a>`
|
||||
);
|
||||
} else {
|
||||
// Link created from selected text; add href and title.
|
||||
$(`a[href=${sStamp}]`, wym._doc.body)
|
||||
.attr(WYMeditor.HREF, returned_item_info.link)
|
||||
.attr(WYMeditor.TITLE, item_info.singular + item_id);
|
||||
}
|
||||
});
|
||||
});
|
||||
Galaxy.modal.hide();
|
||||
},
|
||||
Close: function() {
|
||||
Galaxy.modal.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// EMBED GALAXY OBJECT DIALOGS
|
||||
if (
|
||||
dialogType == CONTROLS.DIALOG_EMBED_HISTORY ||
|
||||
dialogType == CONTROLS.DIALOG_EMBED_DATASET ||
|
||||
dialogType == CONTROLS.DIALOG_EMBED_WORKFLOW ||
|
||||
dialogType == CONTROLS.DIALOG_EMBED_PAGE ||
|
||||
dialogType == CONTROLS.DIALOG_EMBED_VISUALIZATION
|
||||
) {
|
||||
// Based on item type, set useful vars.
|
||||
let item_info;
|
||||
switch (dialogType) {
|
||||
case CONTROLS.DIALOG_EMBED_HISTORY:
|
||||
item_info = get_item_info(CONTROLS.ITEM_HISTORY);
|
||||
break;
|
||||
case CONTROLS.DIALOG_EMBED_DATASET:
|
||||
item_info = get_item_info(CONTROLS.ITEM_DATASET);
|
||||
break;
|
||||
case CONTROLS.DIALOG_EMBED_WORKFLOW:
|
||||
item_info = get_item_info(CONTROLS.ITEM_WORKFLOW);
|
||||
break;
|
||||
case CONTROLS.DIALOG_EMBED_PAGE:
|
||||
item_info = get_item_info(CONTROLS.ITEM_PAGE);
|
||||
break;
|
||||
case CONTROLS.DIALOG_EMBED_VISUALIZATION:
|
||||
item_info = get_item_info(CONTROLS.ITEM_VISUALIZATION);
|
||||
break;
|
||||
}
|
||||
|
||||
const grid = new GridView({
|
||||
url_base: item_info.list_ajax_url,
|
||||
embedded: true
|
||||
});
|
||||
const Galaxy = getGalaxyInstance();
|
||||
Galaxy.modal.show({
|
||||
title: `Insert Link to ${item_info.singular}`,
|
||||
body: $("<div/>")
|
||||
.append(grid.$el)
|
||||
.append(
|
||||
$("<div/>")
|
||||
.append('<input id="make-importable" type="checkbox" checked/>')
|
||||
.append(
|
||||
`Make the selected ${item_info.plural.toLowerCase()} accessible so that they can viewed by everyone.`
|
||||
)
|
||||
),
|
||||
closing_events: true,
|
||||
extra_class: "pages-embed",
|
||||
buttons: {
|
||||
Embed: function() {
|
||||
// Make selected items accessible (importable) ?
|
||||
var make_importable = false;
|
||||
if ($("#make-importable:checked").val() != null) make_importable = true;
|
||||
|
||||
grid.$("input[name=id]:checked").each(function() {
|
||||
var elt = $(this);
|
||||
// Get item ID and name.
|
||||
var item_id = elt.val();
|
||||
// Use ':first' because there are many labels in table; the first one is the item name.
|
||||
var item_name = elt
|
||||
.closest("td")
|
||||
.next("td")
|
||||
.find("label")
|
||||
.text();
|
||||
|
||||
if (make_importable) make_item_importable(item_info.controller, item_id, item_info.singular);
|
||||
|
||||
// Embedded item HTML; item class is embedded in div container classes; this is necessary because the editor strips
|
||||
// all non-standard attributes when it returns its content (e.g. it will not return an element attribute of the form
|
||||
// item_class='History').
|
||||
var item_elt_id = `${item_info.iclass}-${item_id}`;
|
||||
var item_embed_html = [
|
||||
"<div id='",
|
||||
item_elt_id,
|
||||
"' class='embedded-item ",
|
||||
item_info.singular.toLowerCase(),
|
||||
" placeholder'>",
|
||||
"<p class='title'>",
|
||||
"Embedded Galaxy ",
|
||||
item_info.singular,
|
||||
" '",
|
||||
item_name,
|
||||
"'",
|
||||
"</p>",
|
||||
"<p class='content'>",
|
||||
"[Do not edit this block; Galaxy will fill it in with the annotated ",
|
||||
item_info.singular.toLowerCase(),
|
||||
" when it is displayed.]",
|
||||
"</p>",
|
||||
"</div>"
|
||||
].join("");
|
||||
|
||||
// Insert embedded item into document.
|
||||
wym.insert(item_embed_html);
|
||||
});
|
||||
Galaxy.modal.hide();
|
||||
},
|
||||
Close: function() {
|
||||
Galaxy.modal.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function renderEditorWithContent(pageId, content) {
|
||||
const appRoot = getAppRoot();
|
||||
const pageConfigs = {
|
||||
page_id: pageId,
|
||||
page_list_url: `${appRoot}pages/list`,
|
||||
list_objects_url: `${appRoot}page/LIST_ACTION`,
|
||||
set_accessible_url: `${appRoot}ITEM_CONTROLLER/set_accessible_async`,
|
||||
get_name_and_link_url: `${appRoot}ITEM_CONTROLLER/get_name_and_link_async?id=`,
|
||||
editor_base_path: `${appRoot}static/wymeditor/`,
|
||||
iframe_base_path: `${appRoot}static/wymeditor/iframe/galaxy/`
|
||||
};
|
||||
setPageConfigs(pageConfigs);
|
||||
|
||||
// Generic error handling
|
||||
$(document).ajaxError((e, x) => {
|
||||
// console.log( e, x );
|
||||
var message = x.responseText || x.statusText || "Could not connect to server";
|
||||
show_modal("Server error", message, { "Ignore error": hide_modal });
|
||||
return false;
|
||||
});
|
||||
// Create editor
|
||||
$("[name=page_content]")
|
||||
.val(content)
|
||||
.wymeditor({
|
||||
skin: "galaxy",
|
||||
basePath: configs.editor_base_path,
|
||||
iframeBasePath: configs.iframe_base_path,
|
||||
// + WYMeditor.HTML
|
||||
boxHtml: `<table class='wym_box' width='100%' height='100%'><tr><td><div class='wym_area_top'>${
|
||||
WYMeditor.TOOLS
|
||||
}</div></td></tr><tr height='100%'><td><div class='wym_area_main' style='height: 100%;'>${
|
||||
// + WYMeditor.HTML
|
||||
WYMeditor.IFRAME
|
||||
}${WYMeditor.STATUS}</div></div></td></tr></table>`,
|
||||
toolsItems: [
|
||||
{ name: "Bold", title: "Strong", css: "wym_tools_strong" },
|
||||
{ name: "Italic", title: "Emphasis", css: "wym_tools_emphasis" },
|
||||
{
|
||||
name: "Superscript",
|
||||
title: _l("Superscript"),
|
||||
css: "wym_tools_superscript"
|
||||
},
|
||||
{
|
||||
name: "Subscript",
|
||||
title: _l("Subscript"),
|
||||
css: "wym_tools_subscript"
|
||||
},
|
||||
{
|
||||
name: "InsertOrderedList",
|
||||
title: "Ordered_List",
|
||||
css: "wym_tools_ordered_list"
|
||||
},
|
||||
{
|
||||
name: "InsertUnorderedList",
|
||||
title: "Unordered_List",
|
||||
css: "wym_tools_unordered_list"
|
||||
},
|
||||
{ name: "Indent", title: "Indent", css: "wym_tools_indent" },
|
||||
{ name: "Outdent", title: "Outdent", css: "wym_tools_outdent" },
|
||||
{ name: "Undo", title: "Undo", css: "wym_tools_undo" },
|
||||
{ name: "Redo", title: "Redo", css: "wym_tools_redo" },
|
||||
{ name: "CreateLink", title: "Link", css: "wym_tools_link" },
|
||||
{ name: "Unlink", title: "Unlink", css: "wym_tools_unlink" },
|
||||
{ name: "InsertImage", title: "Image", css: "wym_tools_image" },
|
||||
{ name: "InsertTable", title: "Table", css: "wym_tools_table" }
|
||||
]
|
||||
});
|
||||
// Get the editor object
|
||||
editor = $.wymeditors(0);
|
||||
|
||||
//
|
||||
// Containers, Galaxy style
|
||||
//
|
||||
var containers_menu = $(
|
||||
"<div class='galaxy-page-editor-button'><a id='insert-galaxy-link' class='action-button popup' href='#'>Paragraph type</a></div>"
|
||||
);
|
||||
$(".wym_area_top").append(containers_menu);
|
||||
|
||||
// Add menu options.
|
||||
var items = {};
|
||||
$.each(editor._options.containersItems, (k, v) => {
|
||||
var tagname = v.name;
|
||||
items[v.title.replace("_", " ")] = () => {
|
||||
editor.container(tagname);
|
||||
};
|
||||
});
|
||||
make_popupmenu(containers_menu, items);
|
||||
|
||||
//
|
||||
// Create 'Insert Link to Galaxy Object' menu.
|
||||
//
|
||||
|
||||
// Add menu button.
|
||||
var insert_link_menu_button = $(
|
||||
"<div><a id='insert-galaxy-link' class='action-button popup' href='#'>Insert Link to Galaxy Object</a></div>"
|
||||
).addClass("galaxy-page-editor-button");
|
||||
$(".wym_area_top").append(insert_link_menu_button);
|
||||
|
||||
// Add menu options.
|
||||
make_popupmenu(insert_link_menu_button, {
|
||||
"Insert History Link": function() {
|
||||
editor.dialog(CONTROLS.DIALOG_HISTORY_LINK);
|
||||
},
|
||||
"Insert Dataset Link": function() {
|
||||
editor.dialog(CONTROLS.DIALOG_DATASET_LINK);
|
||||
},
|
||||
"Insert Workflow Link": function() {
|
||||
editor.dialog(CONTROLS.DIALOG_WORKFLOW_LINK);
|
||||
},
|
||||
"Insert Page Link": function() {
|
||||
editor.dialog(CONTROLS.DIALOG_PAGE_LINK);
|
||||
},
|
||||
"Insert Visualization Link": function() {
|
||||
editor.dialog(CONTROLS.DIALOG_VISUALIZATION_LINK);
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// Create 'Embed Galaxy Object' menu.
|
||||
//
|
||||
|
||||
// Add menu button.
|
||||
var embed_object_button = $(
|
||||
"<div><a id='embed-galaxy-object' class='action-button popup' href='#'>Embed Galaxy Object</a></div>"
|
||||
).addClass("galaxy-page-editor-button");
|
||||
$(".wym_area_top").append(embed_object_button);
|
||||
|
||||
// Add menu options.
|
||||
make_popupmenu(embed_object_button, {
|
||||
"Embed History": function() {
|
||||
editor.dialog(CONTROLS.DIALOG_EMBED_HISTORY);
|
||||
},
|
||||
"Embed Dataset": function() {
|
||||
editor.dialog(CONTROLS.DIALOG_EMBED_DATASET);
|
||||
},
|
||||
"Embed Workflow": function() {
|
||||
editor.dialog(CONTROLS.DIALOG_EMBED_WORKFLOW);
|
||||
},
|
||||
"Embed Visualization": function() {
|
||||
editor.dialog(CONTROLS.DIALOG_EMBED_VISUALIZATION);
|
||||
}
|
||||
//"Embed Page": function() {
|
||||
// editor.dialog(CONTROLS.DIALOG_EMBED_PAGE);
|
||||
//}
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
pageId: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
content: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.$nextTick(() => {
|
||||
renderEditorWithContent(this.pageId, this.content);
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
saveContent: function() {
|
||||
save(this.pageId, editor.xhtml());
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "embed_item";
|
||||
.galaxy-page-editor-button {
|
||||
position: relative;
|
||||
float: left;
|
||||
padding: 0.2em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as PageEditor } from "./PageEditor.vue";
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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] = `<b>${_.escape(colHeader)}</b>`;
|
||||
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(`<i>${_.escape(mappingDef.columnHeader || mappingDef.label)}</i>`);
|
||||
@@ -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 {
|
||||
|
||||
@@ -6,7 +6,7 @@ export default {
|
||||
props: ["options", "value", "placeholder"],
|
||||
template: `<select><slot></slot></select>`,
|
||||
mounted: function() {
|
||||
var vm = this;
|
||||
const vm = this;
|
||||
$(this.$el)
|
||||
// init select2
|
||||
.select2({ data: this.options, placeholder: this.placeholder, allowClear: this.placeholder })
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
<span id="item-url-text" style="display: none">
|
||||
{{ item_url_parts[0] }}<span id="item-identifier">{{ item_url_parts[1] }}</span>
|
||||
</span>
|
||||
<a href="#" id="edit-identifier"><img :src="pencil_url"/></a>
|
||||
<a href="javascript:void(0)" id="edit-identifier"
|
||||
><img :src="pencil_url" alt="Edit Share Url"
|
||||
/></a>
|
||||
</blockquote>
|
||||
<div v-if="item.published">
|
||||
<p>
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)));
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="loading">
|
||||
<span class="fa fa-spinner fa-spin mb-4 mr-1" />
|
||||
<span>Loading categories...</span>
|
||||
</div>
|
||||
<b-table v-else striped :items="categories" :fields="fields">
|
||||
<template slot="name" slot-scope="data">
|
||||
<b-link href="#" class="font-weight-bold" @click="onCategory(data.value)">
|
||||
{{ data.value }}
|
||||
</b-link>
|
||||
</template>
|
||||
</b-table>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { Services } from "./services.js";
|
||||
export default {
|
||||
props: ["toolshedUrl", "loading"],
|
||||
data() {
|
||||
return {
|
||||
categories: [],
|
||||
fields: {
|
||||
name: {
|
||||
label: "Category",
|
||||
sortable: true
|
||||
},
|
||||
description: {
|
||||
sortable: false
|
||||
},
|
||||
repositories: {
|
||||
sortable: true
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.services = new Services();
|
||||
this.load();
|
||||
},
|
||||
watch: {
|
||||
toolshedUrl() {
|
||||
this.load();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
load() {
|
||||
this.$emit("onLoading", true);
|
||||
this.services
|
||||
.getCategories(this.toolshedUrl)
|
||||
.then(categories => {
|
||||
this.categories = categories;
|
||||
const reducer = (value, entry) => value + entry.repositories;
|
||||
this.$emit("onTotal", this.categories.reduce(reducer, 0));
|
||||
this.$emit("onLoading", false);
|
||||
})
|
||||
.catch(errorMessage => {
|
||||
this.$emit("onError", errorMessage);
|
||||
this.$emit("onLoading", false);
|
||||
});
|
||||
},
|
||||
onCategory(category) {
|
||||
this.$emit("onCategory", category);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div class="overflow-auto h-100 p-1" @scroll="onScroll">
|
||||
<div v-if="error" class="alert alert-danger">{{ error }}</div>
|
||||
<div v-else>
|
||||
<b-input
|
||||
class="mb-3"
|
||||
placeholder="search repositories"
|
||||
v-model="queryInput"
|
||||
@input="delayQuery"
|
||||
@change="setQuery"
|
||||
/>
|
||||
<serverselection
|
||||
:toolshedUrl="toolshedUrl"
|
||||
:toolshedUrls="toolshedUrls"
|
||||
:total="total"
|
||||
:loading="loading"
|
||||
@onToolshed="setToolshed"
|
||||
/>
|
||||
<repositories
|
||||
:query="query"
|
||||
:scrolled="scrolled"
|
||||
:toolshedUrl="toolshedUrl"
|
||||
@onError="setError"
|
||||
v-if="!queryEmpty"
|
||||
/>
|
||||
<categories
|
||||
:toolshedUrl="toolshedUrl"
|
||||
:loading="loading"
|
||||
@onCategory="setQuery"
|
||||
@onTotal="setTotal"
|
||||
@onError="setError"
|
||||
@onLoading="setLoading"
|
||||
v-show="queryEmpty"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { getGalaxyInstance } from "app";
|
||||
import Categories from "./Categories.vue";
|
||||
import Repositories from "./Repositories.vue";
|
||||
import ServerSelection from "./ServerSelection.vue";
|
||||
export default {
|
||||
components: {
|
||||
categories: Categories,
|
||||
repositories: Repositories,
|
||||
serverselection: ServerSelection
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
toolshedUrl: null,
|
||||
toolshedUrls: [],
|
||||
queryInput: null,
|
||||
queryDelay: 1000,
|
||||
queryTimer: null,
|
||||
queryLength: 3,
|
||||
query: null,
|
||||
scrolled: false,
|
||||
loading: false,
|
||||
total: 0,
|
||||
error: null
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.configureToolsheds();
|
||||
},
|
||||
computed: {
|
||||
queryEmpty() {
|
||||
return !this.query || this.query.length < this.queryLength;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
configureToolsheds() {
|
||||
const galaxy = getGalaxyInstance();
|
||||
this.toolshedUrls = galaxy.config.tool_shed_urls;
|
||||
if (!this.toolshedUrls || this.toolshedUrls.length == 0) {
|
||||
this.setError("Toolshed registry is empty, no servers found.");
|
||||
} else {
|
||||
this.toolshedUrl = this.toolshedUrls[0];
|
||||
}
|
||||
},
|
||||
clearTimer() {
|
||||
if (this.queryTimer) {
|
||||
clearTimeout(this.queryTimer);
|
||||
}
|
||||
},
|
||||
delayQuery(query) {
|
||||
this.clearTimer();
|
||||
if (query) {
|
||||
this.queryTimer = setTimeout(() => {
|
||||
this.setQuery(query);
|
||||
}, this.queryDelay);
|
||||
} else {
|
||||
this.setQuery(query);
|
||||
}
|
||||
},
|
||||
setError(error) {
|
||||
this.error = error;
|
||||
},
|
||||
setQuery(query) {
|
||||
this.clearTimer();
|
||||
this.query = this.queryInput = query;
|
||||
},
|
||||
setToolshed(url) {
|
||||
this.toolshedUrl = url;
|
||||
},
|
||||
setTotal(total) {
|
||||
this.total = total;
|
||||
},
|
||||
setLoading(loading) {
|
||||
this.loading = loading;
|
||||
},
|
||||
onScroll: function({ target: { scrollTop, clientHeight, scrollHeight } }) {
|
||||
this.scrolled = scrollTop + clientHeight >= scrollHeight;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-button v-if="installState" :class="buttonClass" variant="primary" @click="onInstall">
|
||||
Install
|
||||
</b-button>
|
||||
<div v-else>
|
||||
<b-button v-if="uninstallState" :class="buttonClass" variant="danger" @click="onUninstall">
|
||||
Uninstall
|
||||
</b-button>
|
||||
<b-button v-else :class="buttonClass" @click="onUninstall">
|
||||
<span v-if="!errorState" class="fa fa-spinner fa-spin" />
|
||||
<span>{{ status }}</span>
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ["installed", "status"],
|
||||
data() {
|
||||
return {
|
||||
buttonClass: "btn-sm text-nowrap"
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
installState() {
|
||||
return !this.status || this.status == "Uninstalled";
|
||||
},
|
||||
uninstallState() {
|
||||
return this.status == "Installed";
|
||||
},
|
||||
errorState() {
|
||||
return this.status == "Error";
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onInstall() {
|
||||
this.$emit("onInstall");
|
||||
},
|
||||
onUninstall() {
|
||||
this.$emit("onUninstall");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<b-modal v-model="modalShow" @ok="onOk" @hide="onHide">
|
||||
<template slot="modal-header">
|
||||
<h4 class="m-0">
|
||||
{{ modalTitle }}
|
||||
</h4>
|
||||
</template>
|
||||
<div class="mb-1">
|
||||
{{ repo.long_description || repo.description }}
|
||||
</div>
|
||||
<div class="text-muted small mb-3">{{ repo.repo_owner_username }} rev. {{ changesetRevision }}</div>
|
||||
<b-form-group
|
||||
v-if="requiresPanel"
|
||||
label="Target Section:"
|
||||
description="Choose an existing tool panel section or create a new section to contain the installed tools (optional)."
|
||||
>
|
||||
<b-form-input list="sectionSelect" v-model="toolSection" />
|
||||
<datalist id="sectionSelect">
|
||||
<option v-for="section in toolSections" :key="section.name">
|
||||
{{ section.name }}
|
||||
</option>
|
||||
</datalist>
|
||||
</b-form-group>
|
||||
<b-link variant="primary" @click="onAdvanced"> {{ advancedTitle }} advanced settings. </b-link>
|
||||
<b-collapse id="advanced-collapse" v-model="advancedShow" class="mt-2">
|
||||
<b-card>
|
||||
<b-form-group v-if="showConfig" label="Tool Configuration:" description="Choose a tool configuration.">
|
||||
<b-form-radio
|
||||
v-for="filename in toolConfigs"
|
||||
v-model="toolConfig"
|
||||
:value="filename"
|
||||
:key="filename"
|
||||
>
|
||||
{{ filename }}
|
||||
</b-form-radio>
|
||||
</b-form-group>
|
||||
<b-form-group label="Dependencies:" description="Choose how to handle dependencies.">
|
||||
<b-form-checkbox v-model="installResolverDependencies">
|
||||
Install resolvable dependencies
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox v-model="installRepositoryDependencies">
|
||||
Install repository dependencies
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox v-model="installToolDependencies">
|
||||
Install tool dependencies
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
</b-card>
|
||||
</b-collapse>
|
||||
</b-modal>
|
||||
</template>
|
||||
<script>
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { Services } from "./services.js";
|
||||
export default {
|
||||
props: ["repo", "changesetRevision", "requiresPanel", "toolshedUrl"],
|
||||
data() {
|
||||
return {
|
||||
modalShow: true,
|
||||
advancedShow: false,
|
||||
installToolDependencies: true,
|
||||
installRepositoryDependencies: true,
|
||||
installResolverDependencies: true,
|
||||
toolConfigs: [],
|
||||
toolConfig: null,
|
||||
toolSections: [],
|
||||
toolSection: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
advancedTitle() {
|
||||
return this.advancedShow ? "Hide" : "Show";
|
||||
},
|
||||
modalTitle() {
|
||||
return `Installing '${this.repo.name}'`;
|
||||
},
|
||||
showConfig() {
|
||||
return this.toolConfigs.length > 1;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.services = new Services();
|
||||
this.loadConfig();
|
||||
},
|
||||
methods: {
|
||||
findSection: function(name) {
|
||||
const result = ['', ''];
|
||||
if (name) {
|
||||
const found = this.toolSections.find(s => {
|
||||
return s.name.toLowerCase().trim() == name.toLowerCase().trim();
|
||||
});
|
||||
if (found) {
|
||||
result[0] = found.id;
|
||||
} else {
|
||||
result[1] = name;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
loadConfig: function() {
|
||||
const galaxy = getGalaxyInstance();
|
||||
const sections = galaxy.config.toolbox_in_panel;
|
||||
this.toolSections = sections.filter(x => x.model_class == "ToolSection");
|
||||
this.toolConfigs = galaxy.config.tool_dynamic_configs || [];
|
||||
if (this.toolConfigs.length > 0) {
|
||||
this.toolConfig = this.toolConfigs[0];
|
||||
}
|
||||
},
|
||||
onAdvanced: function() {
|
||||
this.advancedShow = !this.advancedShow;
|
||||
},
|
||||
onOk: function() {
|
||||
const [sectionId, sectionLabel] = this.findSection(this.toolSection);
|
||||
this.services
|
||||
.installRepository({
|
||||
tool_shed_url: this.toolshedUrl,
|
||||
name: this.repo.name,
|
||||
owner: this.repo.repo_owner_username,
|
||||
changeset_revision: this.changesetRevision,
|
||||
new_tool_panel_section_label: sectionLabel,
|
||||
tool_panel_section_id: sectionId,
|
||||
shed_tool_conf: this.toolConfig,
|
||||
install_resolver_dependencies: this.installResolverDependencies,
|
||||
install_tool_dependencies: this.installToolDependencies,
|
||||
install_repository_dependencies: this.installRepositoryDependencies
|
||||
})
|
||||
.then(response => {
|
||||
window.console.log(response);
|
||||
this.$emit("ok");
|
||||
})
|
||||
.catch(error => {});
|
||||
},
|
||||
onHide: function() {
|
||||
this.$emit("hide");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-table striped :items="repositories" :fields="fields">
|
||||
<template slot="name" slot-scope="row">
|
||||
<b-link href="#" class="font-weight-bold" @click="row.toggleDetails">
|
||||
{{ row.item.name }}
|
||||
</b-link>
|
||||
</template>
|
||||
<template slot="row-details" slot-scope="row">
|
||||
<repositorydetails :repo="row.item" :toolshedUrl="toolshedUrl" />
|
||||
</template>
|
||||
</b-table>
|
||||
<div v-if="noResultsFound">
|
||||
No matching repositories found.
|
||||
</div>
|
||||
<div v-if="pageLoading">
|
||||
<span class="fa fa-spinner fa-spin mb-4" />
|
||||
<span>Loading repositories...</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import RepositoryDetails from "./RepositoryDetails.vue";
|
||||
import { Services } from "./services.js";
|
||||
const READY = 0;
|
||||
const LOADING = 1;
|
||||
const COMPLETE = 2;
|
||||
export default {
|
||||
components: {
|
||||
repositorydetails: RepositoryDetails
|
||||
},
|
||||
props: ["query", "scrolled", "toolshedUrl"],
|
||||
data() {
|
||||
return {
|
||||
repositories: [],
|
||||
fields: [
|
||||
{ key: "name" },
|
||||
{ key: "description" },
|
||||
{ key: "last_updated", label: "Updated" },
|
||||
{ key: "repo_owner_username", label: "Owner" },
|
||||
{ key: "times_downloaded", label: "Downloaded" }
|
||||
],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
pageState: READY,
|
||||
error: null
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
toolshedUrl() {
|
||||
this.load();
|
||||
},
|
||||
query() {
|
||||
this.load();
|
||||
},
|
||||
scrolled() {
|
||||
if (this.scrolled && this.pageState === READY) {
|
||||
this.load(this.page + 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pageLoading() {
|
||||
return this.pageState === LOADING;
|
||||
},
|
||||
noResultsFound() {
|
||||
return this.repositories.length === 0 && !this.pageLoading;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.services = new Services();
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
load(page = 1) {
|
||||
this.page = page;
|
||||
this.pageState = LOADING;
|
||||
if (page == 1) {
|
||||
this.repositories = [];
|
||||
}
|
||||
this.services
|
||||
.getRepositories({
|
||||
tool_shed_url: this.toolshedUrl,
|
||||
q: this.query,
|
||||
page: this.page,
|
||||
page_size: this.pageSize
|
||||
})
|
||||
.then(incoming => {
|
||||
this.repositories = this.repositories.concat(incoming);
|
||||
if (incoming.length < this.pageSize) {
|
||||
this.pageState = COMPLETE;
|
||||
} else {
|
||||
this.pageState = READY;
|
||||
}
|
||||
})
|
||||
.catch(errorMessage => {
|
||||
this.$emit("onError", errorMessage);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<b-card>
|
||||
<div class="mb-1">{{ repo.long_description }}</div>
|
||||
<div class="mb-3">
|
||||
<b-link :href="repo.repository_url" target="_blank">Show additional details and dependencies.</b-link>
|
||||
</div>
|
||||
<div>
|
||||
<span v-if="loading">
|
||||
<span class="fa fa-spinner fa-spin" />
|
||||
Loading repository details...
|
||||
</span>
|
||||
<div v-else>
|
||||
<b-alert v-if="error" variant="danger" show>
|
||||
{{ error }}
|
||||
</b-alert>
|
||||
<div v-else class="border rounded">
|
||||
<b-table borderless :items="repoTable" :fields="repoFields" class="text-center m-0">
|
||||
<template slot="numeric_revision" slot-scope="data">
|
||||
<span class="font-weight-bold">{{ data.value }}</span>
|
||||
</template>
|
||||
<template slot="tools" slot-scope="data">
|
||||
<repositorytools :tools="data.value" />
|
||||
</template>
|
||||
<template slot="profile" slot-scope="data">
|
||||
{{ data.value ? `+${data.value}` : "-" }}
|
||||
</template>
|
||||
<template slot="missing_test_components" slot-scope="data">
|
||||
<span v-if="!data.value" :class="repoChecked" />
|
||||
<span v-else :class="repoUnchecked" />
|
||||
</template>
|
||||
<template slot="actions" slot-scope="row">
|
||||
<installationbutton
|
||||
:installed="row.item.installed"
|
||||
:status="row.item.status"
|
||||
@onInstall="setupRepository(row.item)"
|
||||
@onUninstall="uninstallRepository(row.item)"
|
||||
/>
|
||||
</template>
|
||||
</b-table>
|
||||
<installationsettings
|
||||
v-if="showSettings"
|
||||
:repo="repo"
|
||||
:toolshedUrl="toolshedUrl"
|
||||
:changesetRevision="selectedChangeset"
|
||||
:requiresPanel="selectedRequiresPanel"
|
||||
@hide="onHide"
|
||||
@ok="onOk"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-card>
|
||||
</template>
|
||||
<script>
|
||||
import { Services } from "./services.js";
|
||||
import InstallationSettings from "./InstallationSettings.vue";
|
||||
import InstallationButton from "./InstallationButton.vue";
|
||||
import RepositoryTools from "./RepositoryTools.vue";
|
||||
export default {
|
||||
components: {
|
||||
installationsettings: InstallationSettings,
|
||||
installationbutton: InstallationButton,
|
||||
repositorytools: RepositoryTools
|
||||
},
|
||||
props: ["repo", "toolshedUrl"],
|
||||
data() {
|
||||
return {
|
||||
repoChecked: "fa fa-check text-success",
|
||||
repoUnchecked: "fa fa-times text-danger",
|
||||
selectedChangeset: null,
|
||||
selectedRequiresPanel: false,
|
||||
repoTable: [],
|
||||
repoFields: {
|
||||
numeric_revision: {
|
||||
label: "Revision"
|
||||
},
|
||||
tools: {
|
||||
label: "Tools and Versions"
|
||||
},
|
||||
profile: {
|
||||
label: "Requires"
|
||||
},
|
||||
missing_test_components: {
|
||||
label: "Tests"
|
||||
},
|
||||
actions: {
|
||||
label: ""
|
||||
}
|
||||
},
|
||||
showSettings: false,
|
||||
error: null,
|
||||
loading: true,
|
||||
delay: 2000
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.services = new Services();
|
||||
this.loadDetails();
|
||||
},
|
||||
destroyed() {
|
||||
this.clearTimeout();
|
||||
},
|
||||
methods: {
|
||||
clearTimeout() {
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
},
|
||||
loadDetails() {
|
||||
this.services
|
||||
.getDetails(this.toolshedUrl, this.repo.id)
|
||||
.then(response => {
|
||||
this.repoTable = response;
|
||||
this.loadInstalledRepositories();
|
||||
})
|
||||
.catch(error => {
|
||||
this.error = error;
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
listenInstalledRepositories() {
|
||||
this.clearTimeout();
|
||||
this.timeout = setTimeout(() => {
|
||||
this.loadInstalledRepositories();
|
||||
}, this.delay);
|
||||
},
|
||||
loadInstalledRepositories() {
|
||||
this.services
|
||||
.getInstalledRepositories(this.repo)
|
||||
.then(revisions => {
|
||||
let changed = false;
|
||||
this.repoTable.forEach(x => {
|
||||
const revision = revisions[x.changeset_revision];
|
||||
if (revision && revision.status !== x.status) {
|
||||
x.status = revision.status;
|
||||
x.installed = revision.installed;
|
||||
changed = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (changed) {
|
||||
this.repoTable = [...this.repoTable];
|
||||
}
|
||||
this.listenInstalledRepositories();
|
||||
this.loading = false;
|
||||
})
|
||||
.catch(error => {
|
||||
this.error = error;
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
onOk: function() {
|
||||
this.showSettings = false;
|
||||
},
|
||||
onHide: function() {
|
||||
this.showSettings = false;
|
||||
},
|
||||
setupRepository: function(details) {
|
||||
this.selectedChangeset = details.changeset_revision;
|
||||
this.selectedRequiresPanel = details.includes_tools_for_display_in_tool_panel || details.repository.type == "repository_suite_definition";
|
||||
this.showSettings = true;
|
||||
},
|
||||
uninstallRepository: function(details) {
|
||||
this.services
|
||||
.uninstallRepository({
|
||||
tool_shed_url: this.toolshedUrl,
|
||||
name: this.repo.name,
|
||||
owner: this.repo.repo_owner_username,
|
||||
changeset_revision: details.changeset_revision
|
||||
})
|
||||
.catch(error => {
|
||||
this.error = error;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<table v-if="tools" class="table table-borderless">
|
||||
<tbody>
|
||||
<tr v-if="canExpand && expanded" class="bg-transparent">
|
||||
<td :class="clsFirstColumn">
|
||||
<a href="#" @click.stop.prevent="onExpand">
|
||||
<span class="fa fa-angle-double-up" />
|
||||
<span>Show less</span>
|
||||
</a>
|
||||
</td>
|
||||
<td :class="clsSecondColumn" />
|
||||
</tr>
|
||||
<tr v-for="tool in visibleTools" :key="tool.guid" :class="clsRow">
|
||||
<td :class="clsFirstColumn">
|
||||
{{ tool.id }}
|
||||
</td>
|
||||
<td :class="clsSecondColumn">
|
||||
{{ tool.version }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="canExpand && !expanded" class="bg-transparent">
|
||||
<td :class="clsFirstColumn">
|
||||
<a href="#" @click.stop.prevent="onExpand">
|
||||
<span class="fa fa-angle-double-down" />
|
||||
<span>Show more</span>
|
||||
</a>
|
||||
</td>
|
||||
<td :class="clsSecondColumn" />
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ["tools"],
|
||||
data() {
|
||||
return {
|
||||
clsExpanded: "bg-transparent border-bottom",
|
||||
clsCollapsed: "bg-transparent",
|
||||
clsFirstColumn: "p-0 w-50 text-left",
|
||||
clsSecondColumn: "p-0 w-50 text-center",
|
||||
expanded: false,
|
||||
preview: 2
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
clsRow() {
|
||||
return this.expanded ? this.clsExpanded : this.clsCollapsed;
|
||||
},
|
||||
canExpand() {
|
||||
return this.tools && this.tools.length > this.preview;
|
||||
},
|
||||
visibleTools() {
|
||||
if (this.canExpand && !this.expanded) {
|
||||
return this.tools.slice(0, this.preview);
|
||||
}
|
||||
return this.tools;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onExpand() {
|
||||
this.expanded = !this.expanded;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div v-if="!loading" class="m-1 text-muted">
|
||||
{{ total }} repositories available at
|
||||
<span v-if="showDropdown" class="dropdown">
|
||||
<b-link id="dropdownToolshedUrl" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{{ toolshedUrl }}
|
||||
</b-link>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownToolshedUrl">
|
||||
<a
|
||||
v-for="url in toolshedUrls"
|
||||
:key="url"
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
@click.prevent="onToolshed(url)"
|
||||
>{{ url }}</a
|
||||
>
|
||||
</div>
|
||||
</span>
|
||||
<span v-else>
|
||||
<b-link :href="toolshedUrl" target="_blank">
|
||||
{{ toolshedUrl }}
|
||||
</b-link>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ["toolshedUrl", "toolshedUrls", "loading", "total"],
|
||||
computed: {
|
||||
showDropdown() {
|
||||
return this.toolshedUrls.length > 1;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onToolshed(url) {
|
||||
this.$emit("onToolshed", url);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -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]}&`;
|
||||
}, "");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,378 @@
|
||||
<template>
|
||||
<section class="cloud-auth">
|
||||
<header>
|
||||
<b-alert dismissible fade variant="warning" :show="errorMessage !== null" @dismissed="errorMessage = null">
|
||||
{{ errorMessage }}
|
||||
</b-alert>
|
||||
|
||||
<hgroup class="cloud-auth-title">
|
||||
<h1>Manage Cloud Authorization</h1>
|
||||
<nav class="operations">
|
||||
<ul>
|
||||
<li class="cloudKeyHelp">
|
||||
<a
|
||||
:class="{ active: showHelp }"
|
||||
@click.prevent="showHelp = !showHelp"
|
||||
v-b-tooltip.hover
|
||||
aria-label="Instructions"
|
||||
title="Instructions"
|
||||
>
|
||||
<span>Instructions</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="cloudKeyFilter">
|
||||
<a
|
||||
:class="{ active: showFilter }"
|
||||
@click.prevent="showFilter = !showFilter"
|
||||
v-b-tooltip.hover
|
||||
aria-label="Filter Results"
|
||||
title="Filter Results"
|
||||
>
|
||||
<span>Filter</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="createCloudKey">
|
||||
<a
|
||||
@click.prevent="onCreate"
|
||||
v-b-tooltip.hover
|
||||
aria-label="Create New Key"
|
||||
title="Create New Key"
|
||||
>
|
||||
<span>Create New Key</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</hgroup>
|
||||
|
||||
<transition name="fade">
|
||||
<hgroup v-if="showHelp">
|
||||
<p>See the online <a href="https://galaxyproject.org/cloud/authnz/">Documentation</a>.</p>
|
||||
</hgroup>
|
||||
</transition>
|
||||
|
||||
<transition name="fade">
|
||||
<hgroup v-if="showFilter">
|
||||
<b-form-group :description="filterDescription">
|
||||
<b-form-input type="text" placeholder="Filter" v-model="filter" />
|
||||
</b-form-group>
|
||||
</hgroup>
|
||||
</transition>
|
||||
|
||||
<b-button-group class="mb-4">
|
||||
<button name="createNewKey" @click="onCreate">
|
||||
Create New Authorization Key
|
||||
</button>
|
||||
</b-button-group>
|
||||
</header>
|
||||
|
||||
<div class="scroll-container">
|
||||
<b-list-group>
|
||||
<transition-group name="fade">
|
||||
<cloud-auth-item
|
||||
v-for="credential in filteredItems"
|
||||
:key="credential.counter"
|
||||
:credential="credential"
|
||||
class="mb-1"
|
||||
@delete="onDelete"
|
||||
@save="onSave"
|
||||
@expand="expand(credential, $event)"
|
||||
/>
|
||||
</transition-group>
|
||||
</b-list-group>
|
||||
</div>
|
||||
|
||||
<b-modal
|
||||
v-model="hasDoomed"
|
||||
centered
|
||||
id="deleteCredentialModal"
|
||||
ref="deleteModal"
|
||||
title="Delete Key?"
|
||||
size="sm"
|
||||
@ok="deleteKey"
|
||||
@cancel="doomedItem = null"
|
||||
>
|
||||
</b-modal>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import CloudAuthItem from "./CloudAuthItem";
|
||||
import { Credential } from "./model";
|
||||
import svc from "./model/service";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CloudAuthItem
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
filter: "",
|
||||
showHelp: true,
|
||||
showFilter: false,
|
||||
loading: false,
|
||||
doomedItem: null,
|
||||
errorMessage: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filteredItems() {
|
||||
return this.items.filter(o => o.match(this.filter));
|
||||
},
|
||||
filterDescription() {
|
||||
return `${this.filteredItems.length} matches out of ${this.items.length} items`;
|
||||
},
|
||||
deleteButtonVariant() {
|
||||
return this.showDeleted ? "primary" : "secondary";
|
||||
},
|
||||
hasDoomed: {
|
||||
get() {
|
||||
return this.doomedItem !== null;
|
||||
},
|
||||
// This setter is here because vue-bootstrap modal
|
||||
// tries to set this property for unfathomable reasons
|
||||
set() {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
showDeleted(deleted) {
|
||||
this.loadCredentials({ deleted });
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadCredentials(params = {}) {
|
||||
this.loading = true;
|
||||
svc.listCredentials(params)
|
||||
.then(items => (this.items = items))
|
||||
.catch(this.setError("Unable to load cloud keys."))
|
||||
.finally(() => (this.loading = false));
|
||||
},
|
||||
onCreate() {
|
||||
const newItem = Credential.create({ expanded: true });
|
||||
this.addItem(newItem);
|
||||
},
|
||||
onSave(item) {
|
||||
if (!item.valid) {
|
||||
return;
|
||||
}
|
||||
item.loading = true;
|
||||
svc.saveCredential(item)
|
||||
.then(result => {
|
||||
item.id = result.id;
|
||||
item.updateState();
|
||||
})
|
||||
.catch(this.setError("Unable to save key."))
|
||||
.finally(() => (item.loading = false));
|
||||
},
|
||||
onDelete(doomed) {
|
||||
this.doomedItem = doomed;
|
||||
if (doomed.id) {
|
||||
this.$refs.deleteModal.show();
|
||||
} else {
|
||||
this.removeItem(doomed);
|
||||
this.doomedItem = null;
|
||||
}
|
||||
},
|
||||
deleteKey() {
|
||||
// Called when the modal is closed with an "OK"
|
||||
svc.deleteCredential(this.doomedItem)
|
||||
.then(() => this.removeItem(this.doomedItem))
|
||||
.catch(this.setError("Unable to delete cloud key."))
|
||||
.finally(() => {
|
||||
this.doomedItem = null;
|
||||
});
|
||||
},
|
||||
expand(credential, { expanded }) {
|
||||
credential.expanded = expanded;
|
||||
if (expanded) {
|
||||
this.items.filter(i => i !== credential).forEach(i => (i.expanded = false));
|
||||
}
|
||||
},
|
||||
addItem(item) {
|
||||
this.items = [...this.items, item];
|
||||
},
|
||||
removeItem(item) {
|
||||
this.items = this.items.filter(o => o !== item);
|
||||
},
|
||||
setError(msg) {
|
||||
return err => {
|
||||
this.errorMessage = msg;
|
||||
console.warn(err);
|
||||
};
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadCredentials();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~bootstrap/scss/functions";
|
||||
@import "~bootstrap/scss/variables";
|
||||
@import "~bootstrap/scss/mixins";
|
||||
@import "~bootstrap/scss/utilities/spacing";
|
||||
|
||||
@import "scss/theme/blue.scss";
|
||||
@import "scss/mixins";
|
||||
|
||||
// TODO: build reusable icon menu? Maybe make into a component?
|
||||
// The existing one has lots of reusability problems
|
||||
|
||||
.operations {
|
||||
margin-bottom: 0;
|
||||
|
||||
ul {
|
||||
@include list_reset();
|
||||
display: flex;
|
||||
li:not(:first-child) {
|
||||
@extend .ml-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// General Layout
|
||||
|
||||
.cloud-auth {
|
||||
// header sticks, bottom part scrolls
|
||||
@include scrollingListLayout("header", ".scroll-container");
|
||||
|
||||
// title left, icons right
|
||||
.cloud-auth-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
// top icon menu
|
||||
.operations {
|
||||
li {
|
||||
a,
|
||||
a::before {
|
||||
font-size: 1rem;
|
||||
color: $gray-400;
|
||||
}
|
||||
a.active::before,
|
||||
a:hover::before {
|
||||
color: $brand-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.cloudKeyHelp a {
|
||||
@include fontawesome($fa-var-info-circle);
|
||||
}
|
||||
.cloudKeyFilter a {
|
||||
@include fontawesome($fa-var-filter);
|
||||
}
|
||||
.createCloudKey a {
|
||||
@include fontawesome($fa-var-plus);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Single list item
|
||||
|
||||
.cloud-auth-key {
|
||||
header hgroup {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
> * {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
@extend .my-3;
|
||||
@extend .pt-3;
|
||||
|
||||
// removes wierd double arrows on select
|
||||
.custom-select {
|
||||
background: none;
|
||||
}
|
||||
|
||||
// Undo butchering from base.css so side-by-side labels work
|
||||
.form-row {
|
||||
display: flex;
|
||||
input,
|
||||
select {
|
||||
max-width: none;
|
||||
}
|
||||
label {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
// button list at bottom of form
|
||||
footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
@extend .pt-3;
|
||||
|
||||
button:not(:first-child) {
|
||||
@extend .ml-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// icon menu
|
||||
|
||||
.operations {
|
||||
.save a {
|
||||
@include fontawesome($fa-var-save);
|
||||
}
|
||||
.delete a {
|
||||
@include fontawesome($fa-var-times);
|
||||
}
|
||||
.details a {
|
||||
@include fontawesome($fa-var-window-minimize);
|
||||
&:hover {
|
||||
@include fontawesome($fa-var-window-maximize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reverse icons when key is expanded
|
||||
|
||||
&.expanded .details a {
|
||||
@include fontawesome($fa-var-window-maximize);
|
||||
&:hover {
|
||||
@include fontawesome($fa-var-window-minimize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transitions
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// Delete modal
|
||||
|
||||
#deleteCredentialModal {
|
||||
.modal-body {
|
||||
display: none;
|
||||
}
|
||||
.modal-dialog {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<b-list-group-item class="cloud-auth-key" :class="statusClasses" :variant="variant" :button="!expanded">
|
||||
<header>
|
||||
<hgroup>
|
||||
<h4 @click.prevent="expand()">{{ credential.title }}</h4>
|
||||
<nav class="operations">
|
||||
<ul>
|
||||
<transition name="fade">
|
||||
<li class="save" v-if="expanded && credential.dirty">
|
||||
<a
|
||||
@click.prevent="$emit('save', credential)"
|
||||
v-b-tooltip.hover
|
||||
aria-label="Save Key"
|
||||
title="Save Key"
|
||||
>
|
||||
<span>Save Key</span>
|
||||
</a>
|
||||
</li>
|
||||
</transition>
|
||||
<li class="delete">
|
||||
<a
|
||||
@click.prevent="$emit('delete', credential)"
|
||||
v-b-tooltip.hover
|
||||
aria-label="Delete Key"
|
||||
title="Delete Key"
|
||||
>
|
||||
<span>Delete Key</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="details">
|
||||
<a
|
||||
@click.prevent="expand()"
|
||||
v-b-tooltip.hover
|
||||
aria-label="Show Details"
|
||||
title="Show Details"
|
||||
>
|
||||
<span>Details</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</hgroup>
|
||||
</header>
|
||||
|
||||
<credential-form
|
||||
v-if="expanded"
|
||||
class="border-top"
|
||||
v-model="credential"
|
||||
@click.self="expand()"
|
||||
@save="$emit('save', credential)"
|
||||
@delete="$emit('delete', credential)"
|
||||
/>
|
||||
</b-list-group-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Credential } from "./model";
|
||||
import CredentialForm from "./CredentialForm";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CredentialForm
|
||||
},
|
||||
props: {
|
||||
credential: { type: Credential, required: true }
|
||||
},
|
||||
computed: {
|
||||
statusClasses() {
|
||||
const { expanded, valid, dirty, loading } = this.credential;
|
||||
return { loading, expanded, valid, dirty, collapsed: !expanded };
|
||||
},
|
||||
variant() {
|
||||
if (this.expanded) {
|
||||
return "";
|
||||
}
|
||||
if (this.credential.dirty) {
|
||||
return "warning";
|
||||
}
|
||||
if (!this.credential.valid) {
|
||||
return "danger";
|
||||
}
|
||||
return "primary";
|
||||
},
|
||||
expanded() {
|
||||
return this.credential.expanded;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
expand(forceState) {
|
||||
const expanded = forceState !== undefined ? forceState : !this.expanded;
|
||||
this.$emit("expand", { expanded });
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-form-group
|
||||
v-for="(field, fieldName) in fieldDefs"
|
||||
:key="fieldName"
|
||||
:label="field.label || fieldName"
|
||||
:label-for="'config-' + fieldName"
|
||||
label-cols-lg="3"
|
||||
:state="config.fieldValid(fieldName)"
|
||||
:invalid-feedback="config.errorMessage(fieldName)"
|
||||
>
|
||||
<masked-input
|
||||
:id="'config-' + fieldName"
|
||||
v-model="config[fieldName]"
|
||||
:mask="field.mask"
|
||||
:placeholder="field.placeholder"
|
||||
:maxlength="field.maxlength || Math.Infinity"
|
||||
:state="config.fieldValid(fieldName)"
|
||||
trim
|
||||
/>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MaskedInput from "components/MaskedInput";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MaskedInput
|
||||
},
|
||||
props: {
|
||||
value: { type: Object, required: true }
|
||||
},
|
||||
computed: {
|
||||
config() {
|
||||
return this.value;
|
||||
},
|
||||
fieldDefs() {
|
||||
return this.value.constructor.fields;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<b-form :validated="credential.isValid">
|
||||
<!-- user label -->
|
||||
<b-form-group label="Description" label-for="credentialDescription" label-cols-lg="3">
|
||||
<b-form-input
|
||||
id="credentialDescription"
|
||||
v-model="credential.description"
|
||||
:state="credential.fieldValid('description')"
|
||||
placeholder="Description (optional)"
|
||||
trim
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<!-- google-openidconnect -->
|
||||
<b-form-group
|
||||
label="Identity Provider"
|
||||
label-for="identityProvider"
|
||||
label-cols-lg="3"
|
||||
v-if="identityProviders.length > 1"
|
||||
>
|
||||
<b-form-select
|
||||
id="identityProvider"
|
||||
v-model="credential.authn_id"
|
||||
:state="credential.fieldValid('authn_id')"
|
||||
:options="identityProviders"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<!-- aws/azure, etc -->
|
||||
<b-form-group label="Resource Provider" label-for="resourceProvider" label-cols-lg="3">
|
||||
<b-form-select
|
||||
id="resourceProvider"
|
||||
v-model="credential.resourceProvider"
|
||||
:state="credential.fieldValid('provider')"
|
||||
:options="resourceProviderOptions"
|
||||
/>
|
||||
</b-form-group>
|
||||
|
||||
<credential-config v-model="credential.config" />
|
||||
|
||||
<footer class="border-top">
|
||||
<b-button variant="secondary" @click.prevent="$emit('delete', credential)">
|
||||
Delete Key
|
||||
</b-button>
|
||||
<b-button
|
||||
aria-label="Save Key"
|
||||
:variant="saveButtonVariant"
|
||||
:disabled="saveButtonDisabled"
|
||||
@click.prevent="$emit('save', credential)"
|
||||
>
|
||||
{{ saveButtonTitle }}
|
||||
</b-button>
|
||||
</footer>
|
||||
</b-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Credential, ResourceProviders } from "./model";
|
||||
import { getIdentityProviders } from "./model/service";
|
||||
import CredentialConfig from "./CredentialConfig";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CredentialConfig
|
||||
},
|
||||
props: {
|
||||
value: { type: Credential, required: true }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
identityProviders: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
credential() {
|
||||
return this.value;
|
||||
},
|
||||
// transformed for bootstrap component
|
||||
resourceProviderOptions() {
|
||||
return Array.from(ResourceProviders.entries()).map(([value, o]) => ({ value, text: o.label }));
|
||||
},
|
||||
config() {
|
||||
return this.credential.config;
|
||||
},
|
||||
loading() {
|
||||
return this.credential.loading;
|
||||
},
|
||||
saveButtonDisabled() {
|
||||
return !(this.credential.valid && this.credential.dirty);
|
||||
},
|
||||
saveButtonVariant() {
|
||||
return this.saveButtonDisabled ? "secondary" : "primary";
|
||||
},
|
||||
saveButtonTitle() {
|
||||
return this.loading ? "Saving Key..." : "Save Key";
|
||||
}
|
||||
},
|
||||
created() {
|
||||
getIdentityProviders().then(result => {
|
||||
if (!this.credential.authn_id && result.length == 1) {
|
||||
this.credential.authn_id = result[0].authn_id;
|
||||
this.credential.updateState();
|
||||
}
|
||||
this.identityProviders = result;
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as CloudAuth } from "./CloudAuth.vue";
|
||||
@@ -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"
|
||||
}
|
||||
};
|
||||
@@ -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"));
|
||||
});
|
||||
});
|
||||
@@ -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."
|
||||
}
|
||||
};
|
||||
@@ -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"));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
export { Credential } from "./Credential";
|
||||
export { IdentityProvider } from "./IdentityProvider";
|
||||
export { ResourceProviders } from "./ResourceProviders";
|
||||
@@ -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
|
||||
};
|
||||
+32
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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.";
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -6,18 +6,10 @@
|
||||
<div v-else-if="dataManagers && !dataManagers.length">
|
||||
<Alert variant="primary">
|
||||
<h4 class="alert-heading">None installed</h4>
|
||||
You do not currently have any Data Managers installed
|
||||
</Alert>
|
||||
<Alert variant="info">
|
||||
Data Managers can be installed from a
|
||||
<ToolShedButton />
|
||||
You do not currently have any Data Managers installed.
|
||||
</Alert>
|
||||
</div>
|
||||
<div v-else-if="dataManagers && dataTables">
|
||||
<Alert variant="secondary" dismissible>
|
||||
Choose your data managing option from below. You may install additional Data Managers from a
|
||||
<ToolShedButton />
|
||||
</Alert>
|
||||
<b-container fluid>
|
||||
<b-row>
|
||||
<b-col md="6">
|
||||
@@ -37,7 +29,7 @@
|
||||
</b-row>
|
||||
</b-container>
|
||||
<b-card-group columns>
|
||||
<b-card no-body header="<h4>Data Managers</h4>" id="data-managers-card">
|
||||
<b-card no-body header="Installed Data Managers" id="data-managers-card">
|
||||
<b-list-group flush>
|
||||
<b-list-group-item v-for="(dataManager, index) in dataManagersFiltered" :key="index">
|
||||
<b-button-group vertical>
|
||||
@@ -65,7 +57,7 @@
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</b-card>
|
||||
<b-card no-body header="<h4>Tool Data Tables</h4>">
|
||||
<b-card no-body header="Tool Data Tables">
|
||||
<b-list-group flush>
|
||||
<b-list-group-item
|
||||
v-for="(dataTable, index) in dataTablesFiltered"
|
||||
@@ -91,19 +83,9 @@ import { getAppRoot } from "onload/loadConfig";
|
||||
import axios from "axios";
|
||||
import Alert from "components/Alert.vue";
|
||||
|
||||
var ToolShedButton = {
|
||||
template: `
|
||||
<b-link href="/admin_toolshed/browse_tool_sheds"
|
||||
target="galaxy_main"
|
||||
class="alert-link">
|
||||
ToolShed
|
||||
</b-link>`
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Alert,
|
||||
ToolShedButton
|
||||
Alert
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="mb-3">
|
||||
<span id="data-managers-title">Data Manager</span>
|
||||
<b-badge href="https://galaxyproject.org/admin/tools/data-managers/" target="_blank" variant="secondary"
|
||||
>Wiki</b-badge
|
||||
>
|
||||
</h2>
|
||||
<span class="mb-3">
|
||||
<h2 id="data-managers-title">Local Data</h2>
|
||||
<p>
|
||||
Data Managers are used to manage Galaxy's local data. They can be installed manually, or via the
|
||||
<b-link :href="toolShedLink" target="galaxy_main">ToolShed</b-link>. For more
|
||||
comprehensive information
|
||||
<b-link href="https://galaxyproject.org/admin/tools/data-managers/" target="_blank">See the Wiki</b-link
|
||||
>.
|
||||
</p>
|
||||
</span>
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
|
||||
export default {
|
||||
computed:{
|
||||
toolShedLink() {
|
||||
return `${getAppRoot()}admin_toolshed/browse_tool_sheds`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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.";
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="mb-3">
|
||||
<span id="jobs-title">Jobs</span>
|
||||
</h2>
|
||||
<b-alert v-if="this.message !== ''" :variant="galaxyKwdToBootstrap(status)" show>
|
||||
{{ message }}
|
||||
</b-alert>
|
||||
<b-alert variant="info" show>
|
||||
<p>
|
||||
Unfinished and recently finished jobs are displayed on this page. The 'cutoff' input box will do two
|
||||
things -- it will limit the display of unfinished jobs to only those jobs that have not had their job
|
||||
state updated recently, and it will limit the recently finished jobs list to only displaying jobs that
|
||||
have finished since the cutoff.
|
||||
</p>
|
||||
If any jobs are displayed, you may choose to stop them. Your stop message will be displayed to the user as:
|
||||
"This job was stopped by an administrator: <strong><YOUR MESSAGE></strong> For more information or
|
||||
help, report this error".
|
||||
</b-alert>
|
||||
<b-alert v-if="loading" variant="info" show>
|
||||
Waiting for data
|
||||
</b-alert>
|
||||
<div v-else>
|
||||
<b-container class="mb-3">
|
||||
<b-row>
|
||||
<b-col md="6">
|
||||
<b-form name="jobs" @submit.prevent="onRefresh">
|
||||
<b-form-group
|
||||
id="cutoff"
|
||||
label="Update Jobs"
|
||||
label-for="cutoff-seconds"
|
||||
description="Cutoff in seconds"
|
||||
>
|
||||
<b-input-group>
|
||||
<b-form-input id="cutoff" type="number" placeholder="180" v-model="cutoff">
|
||||
</b-form-input>
|
||||
<b-input-group-append>
|
||||
<b-btn type="submit">Refresh</b-btn>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
</b-form>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-form-group label="Administrative Job Lock" label-for="prevent-job-dispatching">
|
||||
<b-form-checkbox id="prevent-job-dispatching" v-model="jobLock" switch>
|
||||
Job dispatching is currently
|
||||
<strong>{{ jobLockDisplay ? "locked" : "unlocked" }}</strong>
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col md="6">
|
||||
<b-form-group
|
||||
label="Filter Jobs"
|
||||
label-for="filter-regex"
|
||||
description="Search for strings or regular expressions"
|
||||
>
|
||||
<b-input-group id="filter-regex">
|
||||
<b-form-input
|
||||
v-model="filter"
|
||||
placeholder="Type to Search"
|
||||
@keyup.esc.native="filter = ''"
|
||||
/>
|
||||
<b-input-group-append>
|
||||
<b-btn :disabled="!filter" @click="filter = ''">Clear (esc)</b-btn>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
<b-button :pressed.sync="showCommandLine" variant="outline-secondary">
|
||||
{{ showCommandLine ? "Hide" : "Show" }} Command Line
|
||||
</b-button>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-card v-if="jobsItemsComputed.length" header="Stop Selected Jobs">
|
||||
<b-form @submit.prevent="onStopJobs">
|
||||
<b-form-group label-for="stop-message" description="will be displayed to the user">
|
||||
<b-input-group>
|
||||
<b-form-input
|
||||
id="stop-message"
|
||||
v-model="stopMessage"
|
||||
placeholder="Stop message"
|
||||
required
|
||||
>
|
||||
</b-form-input>
|
||||
<b-input-group-append>
|
||||
<b-btn type="submit">Submit</b-btn>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</b-form-group>
|
||||
</b-form>
|
||||
</b-card>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
<b-alert v-if="!jobsItemsComputed.length" variant="secondary" show>
|
||||
There are no unfinished jobs to show with current cutoff time.
|
||||
</b-alert>
|
||||
<b-table
|
||||
v-else
|
||||
:fields="jobsFieldsComputed"
|
||||
:items="jobsItemsComputed"
|
||||
v-model="jobsItemsModel"
|
||||
:filter="filter"
|
||||
hover
|
||||
responsive
|
||||
striped
|
||||
caption-top
|
||||
@row-clicked="showRowDetails"
|
||||
:busy="busy"
|
||||
>
|
||||
<template slot="table-caption">
|
||||
Unfinished Jobs: These jobs are unfinished and have had their state updated in the previous
|
||||
{{ cutoffDisplay }} seconds.
|
||||
</template>
|
||||
<!-- Enable cell formatting for the command line column -->
|
||||
<span slot="html" slot-scope="data" v-html="data.value" />
|
||||
<template slot="HEAD_selected">
|
||||
<b-form-checkbox
|
||||
v-model="allSelected"
|
||||
:indeterminate="indeterminate"
|
||||
@change="toggleAll"
|
||||
></b-form-checkbox>
|
||||
</template>
|
||||
<template slot="selected" slot-scope="data">
|
||||
<b-form-checkbox
|
||||
v-model="selectedStopJobIds"
|
||||
:checked="allSelected"
|
||||
:key="data.index"
|
||||
:value="data.item['job_info']['id']"
|
||||
></b-form-checkbox>
|
||||
</template>
|
||||
<template slot="job_info" slot-scope="data">
|
||||
<b-link :href="data.value['info_url']" target="galaxy_main">
|
||||
{{ data.value["id"] }}
|
||||
</b-link>
|
||||
</template>
|
||||
<template slot="row-details" slot-scope="row">
|
||||
<b-card>
|
||||
<h5>Command Line</h5>
|
||||
<pre
|
||||
class="text-white bg-dark"
|
||||
><code class="break-word">{{ row.item.command_line }}</code></pre>
|
||||
</b-card>
|
||||
</template>
|
||||
</b-table>
|
||||
<b-alert v-if="!recentJobsItemsComputed.length" variant="secondary" show>
|
||||
There are no recently finished jobs to show with current cutoff time.
|
||||
</b-alert>
|
||||
<b-table
|
||||
v-else
|
||||
:fields="recentJobsFieldsComputed"
|
||||
:items="recentJobsItemsComputed"
|
||||
:filter="filter"
|
||||
hover
|
||||
responsive
|
||||
striped
|
||||
caption-top
|
||||
@row-clicked="showRowDetails"
|
||||
:busy="busy"
|
||||
>
|
||||
<template slot="table-caption">
|
||||
Recent Jobs: These jobs have completed in the previous {{ cutoffDisplay }} seconds.
|
||||
</template>
|
||||
<!-- Enable cell formatting for the command line column -->
|
||||
<span slot="html" slot-scope="data" v-html="data.value" />
|
||||
<template slot="job_info" slot-scope="data">
|
||||
<b-link :href="data.value['info_url']" target="galaxy_main">
|
||||
{{ data.value["id"] }}
|
||||
</b-link>
|
||||
</template>
|
||||
<template slot="row-details" slot-scope="row">
|
||||
<b-card>
|
||||
<h5>Command Line</h5>
|
||||
<pre
|
||||
class="text-white bg-dark"
|
||||
><code class="break-word">{{ row.item.command_line }}</code></pre>
|
||||
</b-card>
|
||||
</template>
|
||||
</b-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
jobsItems: [],
|
||||
jobsItemsModel: [],
|
||||
recentJobsItems: [],
|
||||
jobsFields: [
|
||||
{ key: "job_info", label: "Job ID", sortable: true },
|
||||
{ key: "user" },
|
||||
{ key: "tool_id", label: "Tool", tdClass: ["break-word"] },
|
||||
{ key: "state" },
|
||||
{ key: "input_dataset", label: "Inputs" },
|
||||
{ key: "job_runner_name", label: "Job Runner" },
|
||||
{ key: "job_runner_external_id", label: "PID/Cluster ID", sortable: true }
|
||||
],
|
||||
showCommandLine: false,
|
||||
cutoff: 180,
|
||||
cutoffDisplay: 180,
|
||||
jobLock: false,
|
||||
jobLockDisplay: false,
|
||||
selectedStopJobIds: [],
|
||||
allSelected: false,
|
||||
indeterminate: false,
|
||||
stopMessage: "",
|
||||
filter: "",
|
||||
message: "",
|
||||
status: "",
|
||||
loading: true,
|
||||
busy: true
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
jobLock(newVal) {
|
||||
axios
|
||||
.get(`${getAppRoot()}admin/jobs_control?job_lock=${this.jobLock}`)
|
||||
.then(response => {
|
||||
this.jobLock = response.data.job_lock;
|
||||
this.jobLockDisplay = response.data.job_lock;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
selectedStopJobIds(newVal) {
|
||||
if (newVal.length === 0) {
|
||||
this.indeterminate = false;
|
||||
this.allSelected = false;
|
||||
} else if (newVal.length === this.jobsItemsModel.length) {
|
||||
this.indeterminate = false;
|
||||
this.allSelected = true;
|
||||
} else {
|
||||
this.indeterminate = true;
|
||||
this.allSelected = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update() {
|
||||
this.busy = true;
|
||||
let params = [];
|
||||
params.push(`cutoff=${this.cutoff}`);
|
||||
params.push(`stop_msg=${this.stopMessage}`);
|
||||
params.push(`stop=${this.selectedStopJobIds.join()}`);
|
||||
params = params.join("&");
|
||||
axios
|
||||
.get(`${getAppRoot()}admin/jobs_list?${params}`)
|
||||
.then(response => {
|
||||
this.jobsItems = response.data.jobs;
|
||||
this.recentJobsItems = response.data.recent_jobs;
|
||||
this.cutoffDisplay = response.data.cutoff;
|
||||
this.message = response.data.message;
|
||||
this.status = response.data.status;
|
||||
this.jobLock = response.data.job_lock;
|
||||
this.jobLockDisplay = response.data.job_lock;
|
||||
this.loading = false;
|
||||
this.busy = false;
|
||||
})
|
||||
.catch(error => {
|
||||
this.message = error.response.data.err_msg;
|
||||
this.status = "error";
|
||||
console.log(error.response);
|
||||
});
|
||||
},
|
||||
onRefresh() {
|
||||
this.update();
|
||||
},
|
||||
onStopJobs() {
|
||||
this.update();
|
||||
this.selectedStopJobIds = [];
|
||||
this.stopMessage = "";
|
||||
},
|
||||
showRowDetails(row, index, e) {
|
||||
if (e.target.nodeName != "A") {
|
||||
row._showDetails = !row._showDetails;
|
||||
}
|
||||
},
|
||||
translateState(state) {
|
||||
const translateDict = {
|
||||
ok: "success",
|
||||
error: "danger",
|
||||
new: "primary",
|
||||
queued: "secondary",
|
||||
running: "info",
|
||||
upload: "dark"
|
||||
};
|
||||
return translateDict[state] || "primary";
|
||||
},
|
||||
computeItems(items) {
|
||||
return items.map(job => {
|
||||
return {
|
||||
...job,
|
||||
_showDetails: false,
|
||||
_cellVariants: { state: this.translateState(job.state) }
|
||||
};
|
||||
});
|
||||
},
|
||||
computeFields(fields) {
|
||||
const f = Array.from(fields).slice(0);
|
||||
if (this.showCommandLine) {
|
||||
f.splice(6, 0, {
|
||||
key: "command_line",
|
||||
tdClass: ["text-white", "bg-dark", "break-word"]
|
||||
});
|
||||
}
|
||||
return f;
|
||||
},
|
||||
toggleAll(checked) {
|
||||
this.selectedStopJobIds = checked
|
||||
? this.jobsItemsModel.reduce((acc, j) => [...acc, j["job_info"]["id"]], [])
|
||||
: [];
|
||||
},
|
||||
galaxyKwdToBootstrap(status) {
|
||||
let variant = "info";
|
||||
if (status !== "") {
|
||||
variant = status;
|
||||
}
|
||||
const galaxyKwdToBoostrapDict = {
|
||||
done: "success",
|
||||
info: "info",
|
||||
warning: "warning",
|
||||
error: "danger"
|
||||
};
|
||||
if (variant in galaxyKwdToBoostrapDict) {
|
||||
return galaxyKwdToBoostrapDict[variant];
|
||||
} else {
|
||||
return variant;
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
jobsItemsComputed() {
|
||||
return this.computeItems(this.jobsItems);
|
||||
},
|
||||
recentJobsItemsComputed() {
|
||||
return this.computeItems(this.recentJobsItems);
|
||||
},
|
||||
jobsFieldsComputed() {
|
||||
const f = this.jobsFields.slice(0);
|
||||
f.splice(0, 0, { key: "selected", label: "" });
|
||||
f.splice(2, 0, { key: "update_time", label: "Last Update", sortable: true });
|
||||
return this.computeFields(f);
|
||||
},
|
||||
recentJobsFieldsComputed() {
|
||||
const f = this.jobsFields.slice(0);
|
||||
f.splice(2, 0, { key: "update_time", label: "Finished", sortable: true });
|
||||
return this.computeFields(f);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.update();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Can not be scoped because of command line tdClass */
|
||||
.break-word {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
@@ -7,7 +7,7 @@
|
||||
<register
|
||||
:registration_warning_message="registration_warning_message"
|
||||
:mailing_join_addr="mailing_join_addr"
|
||||
:smtp_server="smtp_server"
|
||||
:server_mail_configured="server_mail_configured"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
@@ -57,8 +57,8 @@ export default {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
smtp_server: {
|
||||
type: String,
|
||||
server_mail_configured: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<b-form id="login" @submit.prevent="submitGalaxyLogin()">
|
||||
<b-card no-body header="Welcome to Galaxy, please log in">
|
||||
<b-card-body>
|
||||
<b-form-group label="Username or Email Address">
|
||||
<b-form-group label="Public name or Email Address">
|
||||
<b-form-input name="login" type="text" v-model="login" />
|
||||
</b-form-group>
|
||||
<b-form-group label="Password">
|
||||
@@ -20,12 +20,19 @@
|
||||
</b-card-body>
|
||||
<b-card-footer>
|
||||
Don't have an account?
|
||||
<a id="register-toggle" href="#" @click.prevent="toggleLogin">Register here.</a>
|
||||
<span v-if="allowUserCreation">
|
||||
<a id="register-toggle" href="#" @click.prevent="toggleLogin">Register here.</a>
|
||||
</span>
|
||||
<span v-else>
|
||||
Registration for this Galaxy instance is disabled. Please contact an administrator for
|
||||
assistance.
|
||||
</span>
|
||||
</b-card-footer>
|
||||
</b-card>
|
||||
</b-form>
|
||||
<b-button v-for="idp in oidc_idps" :key="idp" class="d-block mt-3" @click="submitOIDCLogin(idp)">
|
||||
<i v-bind:class="oidc_idps_icons[idp]" /> Sign in with {{ idp.charAt(0).toUpperCase() + idp.slice(1) }}
|
||||
<i v-bind:class="oidc_idps_icons[idp]" /> Sign in with
|
||||
{{ idp.charAt(0).toUpperCase() + idp.slice(1) }}
|
||||
</b-button>
|
||||
</div>
|
||||
<div v-if="show_welcome_with_login" class="col">
|
||||
@@ -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.";
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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.";
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="col col-lg-6">
|
||||
<div class="col" :class="{ 'col-lg-6': !isAdmin }">
|
||||
<b-alert :show="registration_warning_message" variant="danger">
|
||||
{{ registration_warning_message }}
|
||||
</b-alert>
|
||||
@@ -27,12 +27,12 @@
|
||||
('.', '_', '-').</b-form-text
|
||||
>
|
||||
</b-form-group>
|
||||
<b-form-group v-if="mailing_join_addr && smtp_server" label="Subscribe to mailing list">
|
||||
<b-form-group v-if="mailing_join_addr && server_mail_configured" label="Subscribe to mailing list">
|
||||
<input name="subscribe" type="checkbox" v-model="subscribe" />
|
||||
</b-form-group>
|
||||
<b-button name="create" type="submit">Create</b-button>
|
||||
<b-button name="create" type="submit" :disabled="disableCreate">Create</b-button>
|
||||
</b-card-body>
|
||||
<b-card-footer>
|
||||
<b-card-footer v-if="!isAdmin">
|
||||
Already have an account?
|
||||
<a id="login-toggle" href="#" @click.prevent="toggleLogin">Log in here.</a>
|
||||
</b-card-footer>
|
||||
@@ -57,8 +57,8 @@ export default {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
smtp_server: {
|
||||
type: String,
|
||||
server_mail_configured: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
mailing_join_addr: {
|
||||
@@ -71,8 +71,9 @@ export default {
|
||||
}
|
||||
},
|
||||
data() {
|
||||
let galaxy = getGalaxyInstance();
|
||||
const galaxy = getGalaxyInstance();
|
||||
return {
|
||||
disableCreate: false,
|
||||
email: null,
|
||||
password: null,
|
||||
username: null,
|
||||
@@ -80,7 +81,8 @@ export default {
|
||||
subscribe: null,
|
||||
messageText: null,
|
||||
messageVariant: null,
|
||||
session_csrf_token: galaxy.session_csrf_token
|
||||
session_csrf_token: galaxy.session_csrf_token,
|
||||
isAdmin: galaxy.user.isAdmin()
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -95,7 +97,8 @@ export default {
|
||||
}
|
||||
},
|
||||
submit: function(method) {
|
||||
let rootUrl = getAppRoot();
|
||||
this.disableCreate = true;
|
||||
const rootUrl = getAppRoot();
|
||||
axios
|
||||
.post(`${rootUrl}user/create`, this.$data)
|
||||
.then(response => {
|
||||
@@ -105,8 +108,9 @@ export default {
|
||||
window.location = this.redirect || rootUrl;
|
||||
})
|
||||
.catch(error => {
|
||||
this.disableCreate = false;
|
||||
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 || "Registration failed for an unknown reason.";
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,15 +7,17 @@ import QueryStringParsing from "utils/query-string-parsing";
|
||||
import Router from "layout/router";
|
||||
import DataTables from "components/admin/DataTables.vue";
|
||||
import DataTypes from "components/admin/DataTypes.vue";
|
||||
import Jobs from "components/admin/Jobs.vue";
|
||||
import DataManagerView from "components/admin/DataManager/DataManagerView.vue";
|
||||
import DataManagerRouter from "components/admin/DataManager/DataManagerRouter.vue";
|
||||
import Register from "components/login/Register.vue";
|
||||
import ErrorStack from "components/admin/ErrorStack.vue";
|
||||
import DisplayApplications from "components/admin/DisplayApplications.vue";
|
||||
import Toolshed from "components/Toolshed/Index.vue";
|
||||
import Vue from "vue";
|
||||
|
||||
export const getAdminRouter = (Galaxy, options) => {
|
||||
let galaxyRoot = getAppRoot();
|
||||
const galaxyRoot = getAppRoot();
|
||||
|
||||
return Router.extend({
|
||||
routes: {
|
||||
@@ -24,6 +26,7 @@ export const getAdminRouter = (Galaxy, options) => {
|
||||
"(/)admin(/)users(/)create": "show_users_create",
|
||||
"(/)admin(/)roles": "show_roles",
|
||||
"(/)admin(/)groups": "show_groups",
|
||||
"(/)admin(/)toolshed": "show_toolshed",
|
||||
"(/)admin(/)error_stack": "show_error_stack",
|
||||
"(/)admin(/)display_applications": "show_display_applications",
|
||||
"(/)admin(/)tool_versions": "show_tool_versions",
|
||||
@@ -33,12 +36,13 @@ export const getAdminRouter = (Galaxy, options) => {
|
||||
"(/)admin(/)form(/)(:form_id)": "show_form",
|
||||
"(/)admin/data_tables": "show_data_tables",
|
||||
"(/)admin/data_types": "show_data_types",
|
||||
"(/)admin/jobs": "show_jobs",
|
||||
"(/)admin/data_manager*path": "show_data_manager",
|
||||
"*notFound": "not_found"
|
||||
},
|
||||
|
||||
authenticate: function() {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
const Galaxy = getGalaxyInstance();
|
||||
return Galaxy.user && Galaxy.user.id && Galaxy.user.get("is_admin");
|
||||
},
|
||||
|
||||
@@ -57,15 +61,15 @@ export const getAdminRouter = (Galaxy, options) => {
|
||||
},
|
||||
|
||||
show_users_create: function() {
|
||||
let instance = Vue.extend(Register);
|
||||
let vm = document.createElement("div");
|
||||
const instance = Vue.extend(Register);
|
||||
const vm = document.createElement("div");
|
||||
this.page.display(vm);
|
||||
new instance({
|
||||
propsData: {
|
||||
redirect: "/admin/users",
|
||||
registration_warning_message: options.config.registration_warning_message,
|
||||
mailing_join_addr: options.config.mailing_join_addr,
|
||||
smtp_server: options.config.smtp_server
|
||||
server_mail_configured: options.config.server_mail_configured
|
||||
}
|
||||
}).$mount(vm);
|
||||
},
|
||||
@@ -82,6 +86,10 @@ export const getAdminRouter = (Galaxy, options) => {
|
||||
this._show_grid_view("admin_toolshed/browse_repositories");
|
||||
},
|
||||
|
||||
show_toolshed: function() {
|
||||
this._display_vue_helper(Toolshed);
|
||||
},
|
||||
|
||||
show_tool_versions: function() {
|
||||
this._show_grid_view("admin/tool_versions_list");
|
||||
},
|
||||
@@ -91,7 +99,7 @@ export const getAdminRouter = (Galaxy, options) => {
|
||||
},
|
||||
|
||||
_show_grid_view: function(urlSuffix) {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
const Galaxy = getGalaxyInstance();
|
||||
this.page.display(
|
||||
new GridView({
|
||||
url_base: `${galaxyRoot}${urlSuffix}`,
|
||||
@@ -101,8 +109,8 @@ export const getAdminRouter = (Galaxy, options) => {
|
||||
},
|
||||
|
||||
_display_vue_helper: function(component, props) {
|
||||
let instance = Vue.extend(component);
|
||||
let vm = document.createElement("div");
|
||||
const instance = Vue.extend(component);
|
||||
const vm = document.createElement("div");
|
||||
this.page.display(vm);
|
||||
new instance(props).$mount(vm);
|
||||
},
|
||||
@@ -115,6 +123,10 @@ export const getAdminRouter = (Galaxy, options) => {
|
||||
this._display_vue_helper(DataTypes);
|
||||
},
|
||||
|
||||
show_jobs: function() {
|
||||
this._display_vue_helper(Jobs);
|
||||
},
|
||||
|
||||
show_error_stack: function() {
|
||||
this._display_vue_helper(ErrorStack);
|
||||
},
|
||||
@@ -124,9 +136,9 @@ export const getAdminRouter = (Galaxy, options) => {
|
||||
},
|
||||
|
||||
show_data_manager: function(path) {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
const Galaxy = getGalaxyInstance();
|
||||
console.log("show_data_manager");
|
||||
let vueMount = document.createElement("div");
|
||||
const vueMount = document.createElement("div");
|
||||
this.page.display(vueMount);
|
||||
// always set the route back to the base, i.e.
|
||||
// `${galaxyRoot}admin/data_manager`
|
||||
@@ -140,8 +152,8 @@ export const getAdminRouter = (Galaxy, options) => {
|
||||
},
|
||||
|
||||
show_form: function(form_id) {
|
||||
var id = `?id=${QueryStringParsing.get("id")}`;
|
||||
var form_defs = {
|
||||
const id = `?id=${QueryStringParsing.get("id")}`;
|
||||
const form_defs = {
|
||||
reset_user_password: {
|
||||
title: _l("Reset passwords"),
|
||||
url: `admin/reset_user_password${id}`,
|
||||
|
||||
@@ -11,7 +11,7 @@ addInitialization((Galaxy, { options }) => {
|
||||
active_view: "admin"
|
||||
});
|
||||
|
||||
let pageOptions = Object.assign({}, options, {
|
||||
const pageOptions = Object.assign({}, options, {
|
||||
Left: AdminPanel,
|
||||
Router: getAdminRouter(Galaxy, options)
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@ import DatasetEditAttributes from "mvc/dataset/dataset-edit-attributes";
|
||||
import Citations from "components/Citations.vue";
|
||||
import DisplayStructure from "components/DisplayStructured.vue";
|
||||
import Vue from "vue";
|
||||
import { CloudAuth } from "components/User/CloudAuth";
|
||||
|
||||
/** Routes */
|
||||
export const getAnalysisRouter = Galaxy =>
|
||||
@@ -47,6 +48,7 @@ export const getAnalysisRouter = Galaxy =>
|
||||
"(/)root*": "home",
|
||||
"(/)tours(/)(:tour_id)": "show_tours",
|
||||
"(/)user(/)": "show_user",
|
||||
"(/)user(/)cloud_auth": "show_cloud_auth",
|
||||
"(/)user(/)(:form_id)": "show_user_form",
|
||||
"(/)pages(/)create(/)": "show_pages_create",
|
||||
"(/)pages(/)edit(/)": "show_pages_edit",
|
||||
@@ -75,18 +77,19 @@ export const getAnalysisRouter = Galaxy =>
|
||||
"(/)datasets/error": "show_dataset_error"
|
||||
},
|
||||
|
||||
require_login: ["show_user", "show_user_form", "show_workflows"],
|
||||
require_login: ["show_user", "show_user_form", "show_workflows", "show_cloud_auth"],
|
||||
|
||||
authenticate: function(args, name) {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
const Galaxy = getGalaxyInstance();
|
||||
return (Galaxy.user && Galaxy.user.id) || this.require_login.indexOf(name) == -1;
|
||||
},
|
||||
|
||||
_display_vue_helper: function(component, props) {
|
||||
let instance = Vue.extend(component);
|
||||
let vm = document.createElement("div");
|
||||
this.page.display(vm);
|
||||
new instance(props).$mount(vm);
|
||||
_display_vue_helper: function(component, props = {}) {
|
||||
const instance = Vue.extend(component);
|
||||
const container = document.createElement("div");
|
||||
this.page.display(container);
|
||||
const vm = new instance(props).$mount(container);
|
||||
return vm;
|
||||
},
|
||||
|
||||
show_tours: function(tour_id) {
|
||||
@@ -102,15 +105,19 @@ export const getAnalysisRouter = Galaxy =>
|
||||
},
|
||||
|
||||
show_user_form: function(form_id) {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
var model = new UserPreferences.Model({
|
||||
const Galaxy = getGalaxyInstance();
|
||||
const model = new UserPreferences.Model({
|
||||
user_id: Galaxy.params.id
|
||||
});
|
||||
this.page.display(new FormWrapper.View(_.extend(model.get(form_id), { active_tab: "user" })));
|
||||
},
|
||||
|
||||
show_cloud_auth: function() {
|
||||
this._display_vue_helper(CloudAuth);
|
||||
},
|
||||
|
||||
show_visualizations: function(action_id) {
|
||||
var activeTab = action_id == "list_published" ? "shared" : "user";
|
||||
const activeTab = action_id == "list_published" ? "shared" : "user";
|
||||
this.page.display(
|
||||
new GridShared.View({
|
||||
action_id: action_id,
|
||||
@@ -132,8 +139,8 @@ export const getAnalysisRouter = Galaxy =>
|
||||
},
|
||||
|
||||
show_visualizations_sharing: function() {
|
||||
var sharingInstance = Vue.extend(Sharing);
|
||||
var vm = document.createElement("div");
|
||||
const sharingInstance = Vue.extend(Sharing);
|
||||
const vm = document.createElement("div");
|
||||
this.page.display(vm);
|
||||
new sharingInstance({
|
||||
propsData: {
|
||||
@@ -145,7 +152,7 @@ export const getAnalysisRouter = Galaxy =>
|
||||
},
|
||||
|
||||
show_workflows_published: function() {
|
||||
var userFilter = QueryStringParsing.get("f-username");
|
||||
const userFilter = QueryStringParsing.get("f-username");
|
||||
this.page.display(
|
||||
new GridView({
|
||||
url_base: `${getAppRoot()}workflow/list_published`,
|
||||
@@ -158,27 +165,27 @@ export const getAnalysisRouter = Galaxy =>
|
||||
},
|
||||
|
||||
show_history_view: function() {
|
||||
var historyInstance = Vue.extend(HistoryView);
|
||||
var vm = document.createElement("div");
|
||||
const historyInstance = Vue.extend(HistoryView);
|
||||
const vm = document.createElement("div");
|
||||
this.page.display(vm);
|
||||
new historyInstance({ propsData: { id: QueryStringParsing.get("id") } }).$mount(vm);
|
||||
},
|
||||
|
||||
show_history_structure: function() {
|
||||
let displayStructureInstance = Vue.extend(DisplayStructure);
|
||||
let vm = document.createElement("div");
|
||||
const displayStructureInstance = Vue.extend(DisplayStructure);
|
||||
const vm = document.createElement("div");
|
||||
this.page.display(vm);
|
||||
new displayStructureInstance({ propsData: { id: QueryStringParsing.get(" id: ") } }).$mount(vm);
|
||||
},
|
||||
|
||||
show_histories: function(action_id) {
|
||||
let view = new HistoryList.View({ action_id: action_id });
|
||||
const view = new HistoryList.View({ action_id: action_id });
|
||||
this.page.display(view);
|
||||
},
|
||||
|
||||
show_history_citations: function() {
|
||||
var citationInstance = Vue.extend(Citations);
|
||||
var vm = document.createElement("div");
|
||||
const citationInstance = Vue.extend(Citations);
|
||||
const vm = document.createElement("div");
|
||||
this.page.display(vm);
|
||||
new citationInstance({ propsData: { id: QueryStringParsing.get("id"), source: "histories" } }).$mount(vm);
|
||||
},
|
||||
@@ -193,8 +200,8 @@ export const getAnalysisRouter = Galaxy =>
|
||||
},
|
||||
|
||||
show_histories_sharing: function() {
|
||||
var sharingInstance = Vue.extend(Sharing);
|
||||
var vm = document.createElement("div");
|
||||
const sharingInstance = Vue.extend(Sharing);
|
||||
const vm = document.createElement("div");
|
||||
this.page.display(vm);
|
||||
new sharingInstance({
|
||||
propsData: {
|
||||
@@ -228,7 +235,7 @@ export const getAnalysisRouter = Galaxy =>
|
||||
},
|
||||
|
||||
show_pages: function(action_id) {
|
||||
var activeTab = action_id == "list_published" ? "shared" : "user";
|
||||
const activeTab = action_id == "list_published" ? "shared" : "user";
|
||||
this.page.display(
|
||||
new GridShared.View({
|
||||
action_id: action_id,
|
||||
@@ -260,8 +267,8 @@ export const getAnalysisRouter = Galaxy =>
|
||||
},
|
||||
|
||||
show_pages_sharing: function() {
|
||||
var sharingInstance = Vue.extend(Sharing);
|
||||
var vm = document.createElement("div");
|
||||
const sharingInstance = Vue.extend(Sharing);
|
||||
const vm = document.createElement("div");
|
||||
this.page.display(vm);
|
||||
new sharingInstance({
|
||||
propsData: {
|
||||
@@ -273,8 +280,8 @@ export const getAnalysisRouter = Galaxy =>
|
||||
},
|
||||
|
||||
show_plugins: function() {
|
||||
var pluginListInstance = Vue.extend(PluginList);
|
||||
var vm = document.createElement("div");
|
||||
const pluginListInstance = Vue.extend(PluginList);
|
||||
const vm = document.createElement("div");
|
||||
this.page.display(vm);
|
||||
new pluginListInstance().$mount(vm);
|
||||
},
|
||||
@@ -298,14 +305,14 @@ export const getAnalysisRouter = Galaxy =>
|
||||
},
|
||||
|
||||
show_workflows_import: function() {
|
||||
var workflowImportInstance = Vue.extend(WorkflowImport);
|
||||
var vm = document.createElement("div");
|
||||
const workflowImportInstance = Vue.extend(WorkflowImport);
|
||||
const vm = document.createElement("div");
|
||||
this.page.display(vm);
|
||||
new workflowImportInstance().$mount(vm);
|
||||
},
|
||||
|
||||
show_custom_builds: function() {
|
||||
var historyPanel = this.page.historyPanel.historyView;
|
||||
const historyPanel = this.page.historyPanel.historyView;
|
||||
if (!historyPanel || !historyPanel.model || !historyPanel.model.id) {
|
||||
window.setTimeout(() => {
|
||||
this.show_custom_builds();
|
||||
@@ -375,8 +382,8 @@ export const getAnalysisRouter = Galaxy =>
|
||||
this.page.display(new ToolFormComposite.View(_.extend(response, { active_tab: "workflow" })));
|
||||
},
|
||||
error: response => {
|
||||
var error_msg = response.err_msg || "Error occurred while loading the resource.";
|
||||
var options = {
|
||||
const error_msg = response.err_msg || "Error occurred while loading the resource.";
|
||||
const options = {
|
||||
message: error_msg,
|
||||
status: "danger",
|
||||
persistent: true,
|
||||
|
||||
@@ -7,7 +7,7 @@ import Page from "layout/page";
|
||||
addInitialization((Galaxy, { options = {} }) => {
|
||||
console.log("Analysis custom page setup");
|
||||
|
||||
let pageOptions = Object.assign({}, options, {
|
||||
const pageOptions = Object.assign({}, options, {
|
||||
config: Object.assign({}, options.config, {
|
||||
hide_panels: Galaxy.params.hide_panels,
|
||||
hide_masthead: Galaxy.params.hide_masthead
|
||||
|
||||
@@ -7,10 +7,10 @@ import Vue from "vue";
|
||||
export function initLoginView(Galaxy, { options }) {
|
||||
console.log("initLoginView");
|
||||
Galaxy.page = new Page.View(options);
|
||||
var vm = document.createElement("div");
|
||||
const vm = document.createElement("div");
|
||||
Galaxy.display(vm);
|
||||
var component = Galaxy.params.token || Galaxy.params.expired_user ? Password : Index;
|
||||
var loginInstance = Vue.extend(component);
|
||||
const component = Galaxy.params.token || Galaxy.params.expired_user ? Password : Index;
|
||||
const loginInstance = Vue.extend(component);
|
||||
new loginInstance({ propsData: options }).$mount(vm);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,8 @@ import Backbone from "backbone";
|
||||
import _l from "utils/localization";
|
||||
import { getGalaxyInstance } from "app";
|
||||
|
||||
var AdminPanel = Backbone.View.extend({
|
||||
const AdminPanel = Backbone.View.extend({
|
||||
initialize: function(page, options) {
|
||||
var self = this;
|
||||
this.page = page;
|
||||
this.root = options.root;
|
||||
this.config = options.config;
|
||||
@@ -39,6 +38,7 @@ var AdminPanel = Backbone.View.extend({
|
||||
{
|
||||
title: _l("Manage jobs"),
|
||||
url: "admin/jobs",
|
||||
target: "__use_router__",
|
||||
id: "admin-link-jobs"
|
||||
},
|
||||
{
|
||||
@@ -62,7 +62,7 @@ var AdminPanel = Backbone.View.extend({
|
||||
title: _l("Quotas"),
|
||||
url: "admin/quotas",
|
||||
target: "__use_router__",
|
||||
enabled: self.config.enable_quotas,
|
||||
enabled: this.config.enable_quotas,
|
||||
id: "admin-link-quotas"
|
||||
},
|
||||
{
|
||||
@@ -88,25 +88,26 @@ var AdminPanel = Backbone.View.extend({
|
||||
title: _l("Tool Management"),
|
||||
items: [
|
||||
{
|
||||
title: "Install new tools",
|
||||
url: "admin_toolshed/browse_toolsheds",
|
||||
enabled: self.settings.is_tool_shed_installed
|
||||
title: _l("Install or Uninstall"),
|
||||
url: "admin/toolshed",
|
||||
target: "__use_router__",
|
||||
enabled: this.settings.is_tool_shed_installed
|
||||
},
|
||||
{
|
||||
title: _l("Monitor installation"),
|
||||
url: "admin_toolshed/monitor_repository_installation",
|
||||
enabled: self.settings.installing_repository_ids
|
||||
enabled: this.settings.installing_repository_ids
|
||||
},
|
||||
{
|
||||
title: _l("Manage tools"),
|
||||
url: "admin/repositories",
|
||||
enabled: self.settings.is_repo_installed,
|
||||
enabled: this.settings.is_repo_installed,
|
||||
target: "__use_router__"
|
||||
},
|
||||
{
|
||||
title: _l("Manage metadata"),
|
||||
url: "admin_toolshed/reset_metadata_on_selected_installed_repositories",
|
||||
enabled: self.settings.is_repo_installed
|
||||
enabled: this.settings.is_repo_installed
|
||||
},
|
||||
{
|
||||
title: _l("Manage whitelist"),
|
||||
@@ -137,15 +138,14 @@ var AdminPanel = Backbone.View.extend({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
this.$el.empty();
|
||||
this.categories.each(category => {
|
||||
var $section = $(self._templateSection(category.attributes));
|
||||
var $entries = $section.find(".toolSectionBody");
|
||||
const $section = $(this._templateSection(category.attributes));
|
||||
const $entries = $section.find(".toolSectionBody");
|
||||
_.each(category.get("items"), item => {
|
||||
if (item.enabled === undefined || item.enabled) {
|
||||
var $link = $("<a/>")
|
||||
.attr({ href: self.root + item.url })
|
||||
const $link = $("<a/>")
|
||||
.attr({ href: this.root + item.url })
|
||||
.text(_l(item.title));
|
||||
if (item.id) {
|
||||
$link.attr("id", item.id);
|
||||
@@ -153,7 +153,7 @@ var AdminPanel = Backbone.View.extend({
|
||||
if (item.target == "__use_router__") {
|
||||
$link.on("click", e => {
|
||||
e.preventDefault();
|
||||
self.page.router.push(item.url);
|
||||
this.page.router.push(item.url);
|
||||
});
|
||||
} else {
|
||||
$link.attr("target", "galaxy_main");
|
||||
@@ -165,7 +165,7 @@ var AdminPanel = Backbone.View.extend({
|
||||
);
|
||||
}
|
||||
});
|
||||
self.$el.append($section);
|
||||
this.$el.append($section);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import historyOptionsMenu from "mvc/history/options-menu";
|
||||
import CurrentHistoryView from "mvc/history/history-view-edit-current";
|
||||
|
||||
/** the right hand panel in the analysis page that shows the current history */
|
||||
var HistoryPanel = Backbone.View.extend({
|
||||
const HistoryPanel = Backbone.View.extend({
|
||||
initialize: function(page, options) {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
const panelHeaderButtons = [];
|
||||
@@ -38,7 +38,7 @@ var HistoryPanel = Backbone.View.extend({
|
||||
});
|
||||
panelHeaderButtons.push(this.buttonRefresh);
|
||||
|
||||
if (!this.userIsAnonymous){
|
||||
if (!this.userIsAnonymous) {
|
||||
this.buttonNew = new Ui.ButtonLink({
|
||||
id: "history-new-button",
|
||||
title: _l("Create new history"),
|
||||
|
||||
@@ -9,23 +9,23 @@ import { getGalaxyInstance } from "app";
|
||||
import { getAppRoot } from "onload";
|
||||
import Buttons from "mvc/ui/ui-buttons";
|
||||
|
||||
var ToolPanel = Backbone.View.extend({
|
||||
const ToolPanel = Backbone.View.extend({
|
||||
initialize: function(page, options) {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
let appRoot = getAppRoot();
|
||||
const Galaxy = getGalaxyInstance();
|
||||
const appRoot = getAppRoot();
|
||||
|
||||
// access configuration options
|
||||
var config = options.config;
|
||||
const config = options.config;
|
||||
this.root = options.root;
|
||||
|
||||
/** @type {Object[]} descriptions of user's workflows to be shown in the tool menu */
|
||||
this.stored_workflow_menu_entries = config.stored_workflow_menu_entries || [];
|
||||
|
||||
// create tool search, tool panel, and tool panel view.
|
||||
var tool_search = new Tools.ToolSearch({
|
||||
const tool_search = new Tools.ToolSearch({
|
||||
hidden: false
|
||||
});
|
||||
var tools = new Tools.ToolCollection(config.toolbox);
|
||||
const tools = new Tools.ToolCollection(config.toolbox);
|
||||
this.tool_panel = new Tools.ToolPanel({
|
||||
tool_search: tool_search,
|
||||
tools: tools,
|
||||
@@ -43,26 +43,29 @@ var ToolPanel = Backbone.View.extend({
|
||||
default_genome: config.default_genome,
|
||||
default_extension: config.default_extension
|
||||
});
|
||||
const panel_buttons = [this.upload_button];
|
||||
|
||||
// add favorite filter button
|
||||
this.favorite_button = new Buttons.ButtonLink({
|
||||
cls: "panel-header-button",
|
||||
title: _l("Show favorites"),
|
||||
icon: "fa fa-star-o",
|
||||
onclick: e => {
|
||||
$("#tool-search-query")
|
||||
.val("#favorites")
|
||||
.trigger("change");
|
||||
}
|
||||
});
|
||||
|
||||
if (Galaxy.user && Galaxy.user.id) {
|
||||
this.favorite_button = new Buttons.ButtonLink({
|
||||
cls: "panel-header-button",
|
||||
title: _l("Show favorites"),
|
||||
icon: "fa fa-star-o",
|
||||
onclick: e => {
|
||||
$("#tool-search-query")
|
||||
.val("#favorites")
|
||||
.trigger("change");
|
||||
}
|
||||
});
|
||||
panel_buttons.push(this.favorite_button);
|
||||
}
|
||||
// add uploader button to Galaxy object
|
||||
Galaxy.upload = this.upload_button;
|
||||
|
||||
// components for panel definition
|
||||
this.model = new Backbone.Model({
|
||||
title: _l("Tools"),
|
||||
buttons: [this.upload_button, this.favorite_button]
|
||||
buttons: panel_buttons
|
||||
});
|
||||
|
||||
// build body template
|
||||
@@ -71,22 +74,21 @@ var ToolPanel = Backbone.View.extend({
|
||||
|
||||
render: function() {
|
||||
// if there are tools, render panel and display everything
|
||||
var self = this;
|
||||
if (this.tool_panel.get("layout").size() > 0) {
|
||||
this.$el.find(".toolMenu").replaceWith(this.tool_panel_view.$el);
|
||||
this.tool_panel_view.render();
|
||||
}
|
||||
// build the dom for the workflow portion of the tool menu
|
||||
// add internal workflow list
|
||||
self.$("#internal-workflows").append(
|
||||
self._templateAllWorkflow({
|
||||
this.$("#internal-workflows").append(
|
||||
this._templateAllWorkflow({
|
||||
title: _l("All workflows"),
|
||||
href: "workflows/list"
|
||||
})
|
||||
);
|
||||
_.each(this.stored_workflow_menu_entries, menu_entry => {
|
||||
self.$("#internal-workflows").append(
|
||||
self._templateWorkflowLink({
|
||||
this.$("#internal-workflows").append(
|
||||
this._templateWorkflowLink({
|
||||
title: menu_entry.stored_workflow.name,
|
||||
href: `workflows/run?id=${menu_entry.encoded_stored_workflow_id}`
|
||||
})
|
||||
@@ -96,7 +98,7 @@ var ToolPanel = Backbone.View.extend({
|
||||
|
||||
/** build a link to one tool */
|
||||
_templateTool: function(tool) {
|
||||
let appRoot = getAppRoot();
|
||||
const appRoot = getAppRoot();
|
||||
return `<div class="toolTitle">
|
||||
<a href="${appRoot}${tool.href}" target="galaxy_main">
|
||||
${tool.title}
|
||||
@@ -106,7 +108,7 @@ var ToolPanel = Backbone.View.extend({
|
||||
|
||||
/** build a link to 'All Workflows' */
|
||||
_templateAllWorkflow: function(tool) {
|
||||
let appRoot = getAppRoot();
|
||||
const appRoot = getAppRoot();
|
||||
return `<div class="toolTitle">
|
||||
<a href="${appRoot}${tool.href}">
|
||||
${tool.title}
|
||||
@@ -116,7 +118,7 @@ var ToolPanel = Backbone.View.extend({
|
||||
|
||||
/** build links to workflows in toolpanel */
|
||||
_templateWorkflowLink: function(wf) {
|
||||
let appRoot = getAppRoot();
|
||||
const appRoot = getAppRoot();
|
||||
return `<div class="toolTitle">
|
||||
<a class="${wf.cls}" href="${appRoot}${wf.href}">
|
||||
${_.escape(wf.title)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* global $ */
|
||||
/* global toastr */
|
||||
// TODO: this file is transpiled and used directly without bundling; fix imports when that is no longer the case.
|
||||
import $ from "jquery";
|
||||
import { Toast } from "ui/toast";
|
||||
|
||||
/**
|
||||
* Internal function to remove content from the main area and add the notebook.
|
||||
* Not idempotent
|
||||
@@ -35,7 +35,7 @@ export function make_spin_state(
|
||||
sleep_step,
|
||||
log_attempts
|
||||
) {
|
||||
var s = {
|
||||
return {
|
||||
type: typeof type !== "undefined" ? type : "GIE spin",
|
||||
ajax_timeout: typeof ajax_timeout_init !== "undefined" ? ajax_timeout_init : 2000,
|
||||
ajax_timeout_max: typeof ajax_timeout_max !== "undefined" ? ajax_timeout_max : 16000,
|
||||
@@ -46,7 +46,6 @@ export function make_spin_state(
|
||||
log_attempts: typeof log_attempts !== "undefined" ? log_attempts : true,
|
||||
count: 0
|
||||
};
|
||||
return s;
|
||||
}
|
||||
|
||||
/* Log/display an error when spinning fails. */
|
||||
@@ -54,8 +53,8 @@ export function spin_error(console_msg, user_msg, clear) {
|
||||
console.log(console_msg);
|
||||
if (clear) clear_main_area();
|
||||
if (typeof user_msg == "string") {
|
||||
toastr.clear();
|
||||
toastr.error(user_msg, "Error", {
|
||||
Toast.clear();
|
||||
Toast.error(user_msg, "Error", {
|
||||
closeButton: true,
|
||||
timeOut: 0,
|
||||
extendedTimeOut: 0,
|
||||
@@ -112,7 +111,7 @@ export function spin(url, bool_response, success_callback, timeout_callback, err
|
||||
}
|
||||
}
|
||||
};
|
||||
if (bool_response) ajax_params["dataType"] = "json";
|
||||
if (bool_response) ajax_params.dataType = "json";
|
||||
$.ajax(ajax_params);
|
||||
};
|
||||
console.log(`Setting up new spinner for ${spin_state.type} on ${url}`);
|
||||
@@ -131,7 +130,7 @@ function spin_until(url, bool_response, messages, success_callback, spin_state)
|
||||
var message_once = (message, spin_state) => {
|
||||
if (spin_state.count == 1) {
|
||||
display_spinner();
|
||||
toastr.info(message, null, {
|
||||
Toast.info(message, null, {
|
||||
closeButton: true,
|
||||
timeOut: 0,
|
||||
extendedTimeOut: 0,
|
||||
@@ -141,22 +140,22 @@ function spin_until(url, bool_response, messages, success_callback, spin_state)
|
||||
};
|
||||
var wrapped_success = data => {
|
||||
if (!bool_response || (bool_response && data == true)) {
|
||||
console.log(messages["success"]);
|
||||
console.log(messages.success);
|
||||
clear_main_area();
|
||||
toastr.clear();
|
||||
Toast.clear();
|
||||
success_callback();
|
||||
} else if (bool_response && data == false) {
|
||||
message_once(messages["not_ready"], spin_state);
|
||||
message_once(messages.not_ready, spin_state);
|
||||
return false; // keep spinning
|
||||
} else {
|
||||
spin_error(`Invalid response to ${spin_state.type} request`, messages["invalid_response"], true);
|
||||
spin_error(`Invalid response to ${spin_state.type} request`, messages.invalid_response, true);
|
||||
}
|
||||
return true; // stop spinning
|
||||
};
|
||||
var timeout_error = (jqxhr, status, error) => {
|
||||
message_once(messages["waiting"], spin_state);
|
||||
message_once(messages.waiting, spin_state);
|
||||
if (spin_state.count == warn_at) {
|
||||
toastr.warning(messages["wait_warn"], "Warning", {
|
||||
Toast.warning(messages.wait_warn, "Warning", {
|
||||
closeButton: true,
|
||||
timeOut: 0,
|
||||
extendedTimeOut: 0,
|
||||
@@ -235,7 +234,7 @@ export function keepAlive(notebookAccessURL) {
|
||||
if (request_count > 30) {
|
||||
window.clearInterval(interval);
|
||||
clear_main_area();
|
||||
toastr.error("Could not connect to IE, contact your administrator", "Error", {
|
||||
Toast.error("Could not connect to IE, contact your administrator", "Error", {
|
||||
closeButton: true,
|
||||
timeOut: 20000,
|
||||
tapToDismiss: false
|
||||
@@ -245,3 +244,15 @@ export function keepAlive(notebookAccessURL) {
|
||||
});
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
export default {
|
||||
append_notebook,
|
||||
clear_main_area,
|
||||
display_spinner,
|
||||
make_spin_state,
|
||||
spin_error,
|
||||
spin,
|
||||
test_ie_availability,
|
||||
load_when_ready,
|
||||
keepAlive
|
||||
};
|
||||
|
||||
@@ -110,7 +110,7 @@ var GalaxyLibrary = Backbone.View.extend({
|
||||
|
||||
initialize: function() {
|
||||
// This should go upstream in the js app once available
|
||||
let Galaxy = getGalaxyInstance();
|
||||
const Galaxy = getGalaxyInstance();
|
||||
if (Galaxy.config.ga_code) {
|
||||
((i, s, o, g, r, a, m) => {
|
||||
i["GoogleAnalyticsObject"] = r;
|
||||
|
||||
@@ -1,681 +1,31 @@
|
||||
import $ from "jquery";
|
||||
import _l from "utils/localization";
|
||||
import axios from "axios";
|
||||
import { Toast } from "ui/toast";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { PageEditor } from "components/PageEditor";
|
||||
import Vue from "vue";
|
||||
|
||||
import GridView from "mvc/grid/grid-view";
|
||||
import { show_modal, hide_modal } from "layout/modal";
|
||||
import { make_popupmenu } from "ui/popupmenu";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { buildConfig } from "utils/genericConfig";
|
||||
|
||||
// Built a generic config container for the properties that
|
||||
// are passed in from python then used (formerly) globally
|
||||
const configs = buildConfig();
|
||||
|
||||
// Turn these window variables into config props list_objects_url,
|
||||
// set_accessible_url, get_name_and_link_url, editor_base_path,
|
||||
// iframe_base_path, save_url, page_id, page_list_url
|
||||
function setPageConfigs(incoming = {}) {
|
||||
return Object.assign(configs, incoming);
|
||||
}
|
||||
|
||||
var WYMeditor = window.WYMeditor;
|
||||
|
||||
var CONTROLS = {
|
||||
// Item types.
|
||||
ITEM_HISTORY: "item_history",
|
||||
ITEM_DATASET: "item_dataset",
|
||||
ITEM_WORKFLOW: "item_workflow",
|
||||
ITEM_PAGE: "item_page",
|
||||
ITEM_VISUALIZATION: "item_visualization",
|
||||
|
||||
// Link dialogs.
|
||||
DIALOG_HISTORY_LINK: "link_history",
|
||||
DIALOG_DATASET_LINK: "link_dataset",
|
||||
DIALOG_WORKFLOW_LINK: "link_workflow",
|
||||
DIALOG_PAGE_LINK: "link_page",
|
||||
DIALOG_VISUALIZATION_LINK: "link_visualization",
|
||||
|
||||
// Embed dialogs.
|
||||
DIALOG_EMBED_HISTORY: "embed_history",
|
||||
DIALOG_EMBED_DATASET: "embed_dataset",
|
||||
DIALOG_EMBED_WORKFLOW: "embed_workflow",
|
||||
DIALOG_EMBED_PAGE: "embed_page",
|
||||
DIALOG_EMBED_VISUALIZATION: "embed_visualization"
|
||||
};
|
||||
|
||||
// Based on the dialog type, return a dictionary of information about an item
|
||||
function get_item_info(dialog_type) {
|
||||
var item_singular;
|
||||
var item_plural;
|
||||
var item_controller;
|
||||
var item_class;
|
||||
switch (dialog_type) {
|
||||
case CONTROLS.ITEM_HISTORY:
|
||||
item_singular = "History";
|
||||
item_plural = "Histories";
|
||||
item_controller = "history";
|
||||
item_class = "History";
|
||||
break;
|
||||
case CONTROLS.ITEM_DATASET:
|
||||
item_singular = "Dataset";
|
||||
item_plural = "Datasets";
|
||||
item_controller = "dataset";
|
||||
item_class = "HistoryDatasetAssociation";
|
||||
break;
|
||||
case CONTROLS.ITEM_WORKFLOW:
|
||||
item_singular = "Workflow";
|
||||
item_plural = "Workflows";
|
||||
item_controller = "workflow";
|
||||
item_class = "StoredWorkflow";
|
||||
break;
|
||||
case CONTROLS.ITEM_PAGE:
|
||||
item_singular = "Page";
|
||||
item_plural = "Pages";
|
||||
item_controller = "page";
|
||||
item_class = "Page";
|
||||
break;
|
||||
case CONTROLS.ITEM_VISUALIZATION:
|
||||
item_singular = "Visualization";
|
||||
item_plural = "Visualizations";
|
||||
item_controller = "visualization";
|
||||
item_class = "Visualization";
|
||||
break;
|
||||
}
|
||||
|
||||
// Build ajax URL that lists items for selection.
|
||||
var item_list_action = `list_${item_plural.toLowerCase()}_for_selection`;
|
||||
var ajax_url = configs.list_objects_url.replace("LIST_ACTION", item_list_action);
|
||||
|
||||
// Set up and return dict.
|
||||
return {
|
||||
singular: item_singular,
|
||||
plural: item_plural,
|
||||
controller: item_controller,
|
||||
iclass: item_class,
|
||||
list_ajax_url: ajax_url
|
||||
};
|
||||
}
|
||||
|
||||
// Make an item importable.
|
||||
function make_item_importable(item_controller, item_id, item_type) {
|
||||
var ajax_url = configs.set_accessible_url.replace("ITEM_CONTROLLER", item_controller);
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: ajax_url,
|
||||
data: { id: item_id, accessible: "True" },
|
||||
error: function() {
|
||||
alert(`Making ${item_type} accessible failed`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Completely replace WYM's dialog handling
|
||||
WYMeditor.editor.prototype.dialog = function(dialogType, dialogFeatures, bodyHtml) {
|
||||
var wym = this;
|
||||
var sStamp = wym.uniqueStamp();
|
||||
var selected = wym.selected();
|
||||
|
||||
// Swap out URL attribute for id/name attribute in link creation to enable anchor creation in page.
|
||||
function set_link_id() {
|
||||
// When "set link id" link clicked, update UI.
|
||||
$("#set_link_id").click(function() {
|
||||
// Set label.
|
||||
$("#link_attribute_label").text("ID/Name");
|
||||
|
||||
// Set input elt class, value.
|
||||
var attribute_input = $(".wym_href");
|
||||
attribute_input.addClass("wym_id").removeClass("wym_href");
|
||||
if (selected) attribute_input.val($(selected).attr("id"));
|
||||
|
||||
// Remove link.
|
||||
$(this).remove();
|
||||
});
|
||||
}
|
||||
|
||||
// LINK DIALOG
|
||||
if (dialogType == WYMeditor.DIALOG_LINK) {
|
||||
if (selected) {
|
||||
$(wym._options.hrefSelector).val($(selected).attr(WYMeditor.HREF));
|
||||
$(wym._options.srcSelector).val($(selected).attr(WYMeditor.SRC));
|
||||
$(wym._options.titleSelector).val($(selected).attr(WYMeditor.TITLE));
|
||||
$(wym._options.altSelector).val($(selected).attr(WYMeditor.ALT));
|
||||
}
|
||||
|
||||
// Get current URL, title.
|
||||
var curURL;
|
||||
|
||||
var curTitle;
|
||||
if (selected) {
|
||||
curURL = $(selected).attr("href");
|
||||
if (curURL == undefined) curURL = "";
|
||||
curTitle = $(selected).attr("title");
|
||||
if (curTitle == undefined) curTitle = "";
|
||||
}
|
||||
show_modal(
|
||||
"Create Link",
|
||||
`<div><div><label id='link_attribute_label'>URL <span style='float: right; font-size: 90%'><a href='#' id='set_link_id'>Create in-page anchor</a></span></label><br><input type='text' class='wym_href' value='${curURL}' size='40' /></div><div><label>Title</label><br><input type='text' class='wym_title' value='${curTitle}' size='40' /></div><div>`,
|
||||
{
|
||||
"Make link": function() {
|
||||
// Get URL, name/title.
|
||||
var sUrl = $(wym._options.hrefSelector).val() || "";
|
||||
|
||||
var sId = $(".wym_id").val() || "";
|
||||
var sName = $(wym._options.titleSelector).val() || "";
|
||||
|
||||
if (sUrl || sId) {
|
||||
// Create link.
|
||||
wym._exec(WYMeditor.CREATE_LINK, sStamp);
|
||||
|
||||
// Set link attributes.
|
||||
var link = $(`a[href=${sStamp}]`, wym._doc.body);
|
||||
link.attr(WYMeditor.HREF, sUrl)
|
||||
.attr(WYMeditor.TITLE, sName)
|
||||
.attr("id", sId);
|
||||
|
||||
// If link's text is default (wym-...), change it to the title.
|
||||
if (link.text().indexOf("wym-") === 0) {
|
||||
link.text(sName);
|
||||
}
|
||||
}
|
||||
hide_modal();
|
||||
},
|
||||
Cancel: function() {
|
||||
hide_modal();
|
||||
}
|
||||
},
|
||||
{},
|
||||
set_link_id
|
||||
);
|
||||
}
|
||||
|
||||
// IMAGE DIALOG
|
||||
if (dialogType == WYMeditor.DIALOG_IMAGE) {
|
||||
if (wym._selected_image) {
|
||||
$(`${wym._options.dialogImageSelector} ${wym._options.srcSelector}`).val(
|
||||
$(wym._selected_image).attr(WYMeditor.SRC)
|
||||
);
|
||||
$(`${wym._options.dialogImageSelector} ${wym._options.titleSelector}`).val(
|
||||
$(wym._selected_image).attr(WYMeditor.TITLE)
|
||||
);
|
||||
$(`${wym._options.dialogImageSelector} ${wym._options.altSelector}`).val(
|
||||
$(wym._selected_image).attr(WYMeditor.ALT)
|
||||
);
|
||||
}
|
||||
show_modal(
|
||||
"Image",
|
||||
"<div class='row'>" +
|
||||
"<label>URL</label><br>" +
|
||||
"<input type='text' class='wym_src' value='' size='40' />" +
|
||||
"</div>" +
|
||||
"<div class='row'>" +
|
||||
"<label>Alt text</label><br>" +
|
||||
"<input type='text' class='wym_alt' value='' size='40' />" +
|
||||
"</div>" +
|
||||
"<div class='row'>" +
|
||||
"<label>Title</label><br>" +
|
||||
"<input type='text' class='wym_title' value='' size='40' />" +
|
||||
"</div>",
|
||||
{
|
||||
Insert: function() {
|
||||
var sUrl = $(wym._options.srcSelector).val();
|
||||
if (sUrl.length > 0) {
|
||||
wym._exec(WYMeditor.INSERT_IMAGE, sStamp);
|
||||
$(`img[src$=${sStamp}]`, wym._doc.body)
|
||||
.attr(WYMeditor.SRC, sUrl)
|
||||
.attr(WYMeditor.TITLE, $(wym._options.titleSelector).val())
|
||||
.attr(WYMeditor.ALT, $(wym._options.altSelector).val());
|
||||
}
|
||||
hide_modal();
|
||||
},
|
||||
Cancel: function() {
|
||||
hide_modal();
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// TABLE DIALOG
|
||||
if (dialogType == WYMeditor.DIALOG_TABLE) {
|
||||
show_modal(
|
||||
"Table",
|
||||
"<div class='row'>" +
|
||||
"<label>Caption</label><br>" +
|
||||
"<input type='text' class='wym_caption' value='' size='40' />" +
|
||||
"</div>" +
|
||||
"<div class='row'>" +
|
||||
"<label>Summary</label><br>" +
|
||||
"<input type='text' class='wym_summary' value='' size='40' />" +
|
||||
"</div>" +
|
||||
"<div class='row'>" +
|
||||
"<label>Number Of Rows<br></label>" +
|
||||
"<input type='text' class='wym_rows' value='3' size='3' />" +
|
||||
"</div>" +
|
||||
"<div class='row'>" +
|
||||
"<label>Number Of Cols<br></label>" +
|
||||
"<input type='text' class='wym_cols' value='2' size='3' />" +
|
||||
"</div>",
|
||||
{
|
||||
Insert: function() {
|
||||
var iRows = $(wym._options.rowsSelector).val();
|
||||
var iCols = $(wym._options.colsSelector).val();
|
||||
|
||||
if (iRows > 0 && iCols > 0) {
|
||||
var table = wym._doc.createElement(WYMeditor.TABLE);
|
||||
var newRow = null;
|
||||
|
||||
var sCaption = $(wym._options.captionSelector).val();
|
||||
|
||||
//we create the caption
|
||||
var newCaption = table.createCaption();
|
||||
newCaption.innerHTML = sCaption;
|
||||
|
||||
//we create the rows and cells
|
||||
for (var x = 0; x < iRows; x++) {
|
||||
newRow = table.insertRow(x);
|
||||
for (var y = 0; y < iCols; y++) {
|
||||
newRow.insertCell(y);
|
||||
}
|
||||
}
|
||||
|
||||
//set the summary attr
|
||||
$(table).attr("summary", $(wym._options.summarySelector).val());
|
||||
|
||||
//append the table after the selected container
|
||||
var node = $(wym.findUp(wym.container(), WYMeditor.MAIN_CONTAINERS)).get(0);
|
||||
if (!node || !node.parentNode) $(wym._doc.body).append(table);
|
||||
else $(node).after(table);
|
||||
}
|
||||
hide_modal();
|
||||
export default function pagesEditorOnload() {
|
||||
const pageId = $("#page-editor-content").attr("page_id");
|
||||
axios
|
||||
.get(`${getAppRoot()}api/pages/${pageId}`)
|
||||
.then(response => {
|
||||
const pageEditorInstance = Vue.extend(PageEditor);
|
||||
new pageEditorInstance({
|
||||
propsData: {
|
||||
pageId: pageId,
|
||||
content: response.data.content,
|
||||
title: response.data.title
|
||||
},
|
||||
Cancel: function() {
|
||||
hide_modal();
|
||||
}
|
||||
el: "#page-editor-content"
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
const response = e.response;
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
Toast.error(response.responseJSON.err_msg);
|
||||
} else {
|
||||
Toast.error("An error occurred.");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// INSERT "GALAXY ITEM" LINK DIALOG
|
||||
if (
|
||||
dialogType == CONTROLS.DIALOG_HISTORY_LINK ||
|
||||
dialogType == CONTROLS.DIALOG_DATASET_LINK ||
|
||||
dialogType == CONTROLS.DIALOG_WORKFLOW_LINK ||
|
||||
dialogType == CONTROLS.DIALOG_PAGE_LINK ||
|
||||
dialogType == CONTROLS.DIALOG_VISUALIZATION_LINK
|
||||
) {
|
||||
// Based on item type, set useful vars.
|
||||
var item_info;
|
||||
switch (dialogType) {
|
||||
case CONTROLS.DIALOG_HISTORY_LINK:
|
||||
item_info = get_item_info(CONTROLS.ITEM_HISTORY);
|
||||
break;
|
||||
case CONTROLS.DIALOG_DATASET_LINK:
|
||||
item_info = get_item_info(CONTROLS.ITEM_DATASET);
|
||||
break;
|
||||
case CONTROLS.DIALOG_WORKFLOW_LINK:
|
||||
item_info = get_item_info(CONTROLS.ITEM_WORKFLOW);
|
||||
break;
|
||||
case CONTROLS.DIALOG_PAGE_LINK:
|
||||
item_info = get_item_info(CONTROLS.ITEM_PAGE);
|
||||
break;
|
||||
case CONTROLS.DIALOG_VISUALIZATION_LINK:
|
||||
item_info = get_item_info(CONTROLS.ITEM_VISUALIZATION);
|
||||
break;
|
||||
}
|
||||
|
||||
var grid = new GridView({
|
||||
url_base: item_info.list_ajax_url,
|
||||
embedded: true
|
||||
});
|
||||
let Galaxy = getGalaxyInstance();
|
||||
Galaxy.modal.show({
|
||||
title: `Insert Link to ${item_info.singular}`,
|
||||
body: $("<div/>")
|
||||
.append(grid.$el)
|
||||
.append(
|
||||
$("<div/>")
|
||||
.append('<input id="make-importable" type="checkbox" checked/>')
|
||||
.append(
|
||||
`Make the selected ${item_info.plural.toLowerCase()} accessible so that they can viewed by everyone.`
|
||||
)
|
||||
),
|
||||
closing_events: true,
|
||||
buttons: {
|
||||
Insert: function() {
|
||||
// Make selected items accessible (importable) ?
|
||||
var make_importable = false;
|
||||
if ($("#make-importable:checked").val() != null) make_importable = true;
|
||||
|
||||
// Insert links to history for each checked item.
|
||||
grid.$("input[name=id]:checked").each(function() {
|
||||
var item_id = $(this).val();
|
||||
|
||||
// Make item importable?
|
||||
if (make_importable) make_item_importable(item_info.controller, item_id, item_info.singular);
|
||||
|
||||
// Insert link(s) to item(s). This is done by getting item info and then manipulating wym.
|
||||
var url_template = configs.get_name_and_link_url + item_id;
|
||||
var ajax_url = url_template.replace("ITEM_CONTROLLER", item_info.controller);
|
||||
$.getJSON(ajax_url, returned_item_info => {
|
||||
// Get link text.
|
||||
wym._exec(WYMeditor.CREATE_LINK, sStamp);
|
||||
var link_text = $(`a[href=${sStamp}]`, wym._doc.body).text();
|
||||
|
||||
// Insert link: need to do different actions depending on link text.
|
||||
if (
|
||||
link_text == "" || // Firefox.
|
||||
link_text == sStamp // Safari
|
||||
) {
|
||||
// User selected no text; create link from scratch and use default text.
|
||||
wym.insert(
|
||||
`<a href='${returned_item_info.link}'>${item_info.singular} '${
|
||||
returned_item_info.name
|
||||
}'</a>`
|
||||
);
|
||||
} else {
|
||||
// Link created from selected text; add href and title.
|
||||
$(`a[href=${sStamp}]`, wym._doc.body)
|
||||
.attr(WYMeditor.HREF, returned_item_info.link)
|
||||
.attr(WYMeditor.TITLE, item_info.singular + item_id);
|
||||
}
|
||||
});
|
||||
});
|
||||
Galaxy.modal.hide();
|
||||
},
|
||||
Close: function() {
|
||||
Galaxy.modal.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// EMBED GALAXY OBJECT DIALOGS
|
||||
if (
|
||||
dialogType == CONTROLS.DIALOG_EMBED_HISTORY ||
|
||||
dialogType == CONTROLS.DIALOG_EMBED_DATASET ||
|
||||
dialogType == CONTROLS.DIALOG_EMBED_WORKFLOW ||
|
||||
dialogType == CONTROLS.DIALOG_EMBED_PAGE ||
|
||||
dialogType == CONTROLS.DIALOG_EMBED_VISUALIZATION
|
||||
) {
|
||||
// Based on item type, set useful vars.
|
||||
let item_info;
|
||||
switch (dialogType) {
|
||||
case CONTROLS.DIALOG_EMBED_HISTORY:
|
||||
item_info = get_item_info(CONTROLS.ITEM_HISTORY);
|
||||
break;
|
||||
case CONTROLS.DIALOG_EMBED_DATASET:
|
||||
item_info = get_item_info(CONTROLS.ITEM_DATASET);
|
||||
break;
|
||||
case CONTROLS.DIALOG_EMBED_WORKFLOW:
|
||||
item_info = get_item_info(CONTROLS.ITEM_WORKFLOW);
|
||||
break;
|
||||
case CONTROLS.DIALOG_EMBED_PAGE:
|
||||
item_info = get_item_info(CONTROLS.ITEM_PAGE);
|
||||
break;
|
||||
case CONTROLS.DIALOG_EMBED_VISUALIZATION:
|
||||
item_info = get_item_info(CONTROLS.ITEM_VISUALIZATION);
|
||||
break;
|
||||
}
|
||||
|
||||
let grid = new GridView({
|
||||
url_base: item_info.list_ajax_url,
|
||||
embedded: true
|
||||
});
|
||||
let Galaxy = getGalaxyInstance();
|
||||
Galaxy.modal.show({
|
||||
title: `Insert Link to ${item_info.singular}`,
|
||||
body: $("<div/>")
|
||||
.append(grid.$el)
|
||||
.append(
|
||||
$("<div/>")
|
||||
.append('<input id="make-importable" type="checkbox" checked/>')
|
||||
.append(
|
||||
`Make the selected ${item_info.plural.toLowerCase()} accessible so that they can viewed by everyone.`
|
||||
)
|
||||
),
|
||||
closing_events: true,
|
||||
buttons: {
|
||||
Embed: function() {
|
||||
// Make selected items accessible (importable) ?
|
||||
var make_importable = false;
|
||||
if ($("#make-importable:checked").val() != null) make_importable = true;
|
||||
|
||||
grid.$("input[name=id]:checked").each(function() {
|
||||
var elt = $(this);
|
||||
// Get item ID and name.
|
||||
var item_id = elt.val();
|
||||
// Use ':first' because there are many labels in table; the first one is the item name.
|
||||
var item_name = elt
|
||||
.closest("td")
|
||||
.next("td")
|
||||
.find("label")
|
||||
.text();
|
||||
|
||||
if (make_importable) make_item_importable(item_info.controller, item_id, item_info.singular);
|
||||
|
||||
// Embedded item HTML; item class is embedded in div container classes; this is necessary because the editor strips
|
||||
// all non-standard attributes when it returns its content (e.g. it will not return an element attribute of the form
|
||||
// item_class='History').
|
||||
var item_elt_id = `${item_info.iclass}-${item_id}`;
|
||||
var item_embed_html = [
|
||||
"<div id='",
|
||||
item_elt_id,
|
||||
"' class='embedded-item ",
|
||||
item_info.singular.toLowerCase(),
|
||||
" placeholder'>",
|
||||
"<p class='title'>",
|
||||
"Embedded Galaxy ",
|
||||
item_info.singular,
|
||||
" '",
|
||||
item_name,
|
||||
"'",
|
||||
"</p>",
|
||||
"<p class='content'>",
|
||||
"[Do not edit this block; Galaxy will fill it in with the annotated ",
|
||||
item_info.singular.toLowerCase(),
|
||||
" when it is displayed.]",
|
||||
"</p>",
|
||||
"</div>"
|
||||
].join("");
|
||||
|
||||
// Insert embedded item into document.
|
||||
wym.insert(item_embed_html);
|
||||
});
|
||||
Galaxy.modal.hide();
|
||||
},
|
||||
Close: function() {
|
||||
Galaxy.modal.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default function pagesEditorOnload(renderedConfigs = {}) {
|
||||
setPageConfigs(renderedConfigs);
|
||||
|
||||
// Generic error handling
|
||||
$(document).ajaxError((e, x) => {
|
||||
// console.log( e, x );
|
||||
var message = x.responseText || x.statusText || "Could not connect to server";
|
||||
show_modal("Server error", message, { "Ignore error": hide_modal });
|
||||
return false;
|
||||
});
|
||||
// Create editor
|
||||
$("[name=page_content]").wymeditor({
|
||||
skin: "galaxy",
|
||||
basePath: configs.editor_base_path,
|
||||
iframeBasePath: configs.iframe_base_path,
|
||||
// + WYMeditor.HTML
|
||||
boxHtml: `<table class='wym_box' width='100%' height='100%'><tr><td><div class='wym_area_top'>${
|
||||
WYMeditor.TOOLS
|
||||
}</div></td></tr><tr height='100%'><td><div class='wym_area_main' style='height: 100%;'>${
|
||||
// + WYMeditor.HTML
|
||||
WYMeditor.IFRAME
|
||||
}${WYMeditor.STATUS}</div></div></td></tr></table>`,
|
||||
toolsItems: [
|
||||
{ name: "Bold", title: "Strong", css: "wym_tools_strong" },
|
||||
{ name: "Italic", title: "Emphasis", css: "wym_tools_emphasis" },
|
||||
{
|
||||
name: "Superscript",
|
||||
title: _l("Superscript"),
|
||||
css: "wym_tools_superscript"
|
||||
},
|
||||
{
|
||||
name: "Subscript",
|
||||
title: _l("Subscript"),
|
||||
css: "wym_tools_subscript"
|
||||
},
|
||||
{
|
||||
name: "InsertOrderedList",
|
||||
title: "Ordered_List",
|
||||
css: "wym_tools_ordered_list"
|
||||
},
|
||||
{
|
||||
name: "InsertUnorderedList",
|
||||
title: "Unordered_List",
|
||||
css: "wym_tools_unordered_list"
|
||||
},
|
||||
{ name: "Indent", title: "Indent", css: "wym_tools_indent" },
|
||||
{ name: "Outdent", title: "Outdent", css: "wym_tools_outdent" },
|
||||
{ name: "Undo", title: "Undo", css: "wym_tools_undo" },
|
||||
{ name: "Redo", title: "Redo", css: "wym_tools_redo" },
|
||||
{ name: "CreateLink", title: "Link", css: "wym_tools_link" },
|
||||
{ name: "Unlink", title: "Unlink", css: "wym_tools_unlink" },
|
||||
{ name: "InsertImage", title: "Image", css: "wym_tools_image" },
|
||||
{ name: "InsertTable", title: "Table", css: "wym_tools_table" }
|
||||
]
|
||||
});
|
||||
// Get the editor object
|
||||
var editor = $.wymeditors(0);
|
||||
var save = callback => {
|
||||
show_modal("Saving page", "progress");
|
||||
|
||||
// Do save.
|
||||
$.ajax({
|
||||
url: configs.save_url,
|
||||
type: "POST",
|
||||
data: {
|
||||
id: configs.page_id,
|
||||
content: editor.xhtml(),
|
||||
annotations: JSON.stringify({}),
|
||||
// annotations: JSON.stringify(annotations),
|
||||
_: "true"
|
||||
},
|
||||
success: function() {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
};
|
||||
// Save button
|
||||
$("#save-button").click(() => {
|
||||
save(() => {
|
||||
hide_modal();
|
||||
});
|
||||
});
|
||||
// Close button
|
||||
$("#close-button").click(() => {
|
||||
// var new_content = editor.xhtml();
|
||||
// var changed = ( initial_content != new_content );
|
||||
var changed = false;
|
||||
if (changed) {
|
||||
var do_close = () => {
|
||||
window.onbeforeunload = undefined;
|
||||
window.document.location = configs.page_list_url;
|
||||
};
|
||||
show_modal(
|
||||
"Close editor",
|
||||
"There are unsaved changes to your page which will be lost.",
|
||||
{
|
||||
Cancel: hide_modal,
|
||||
"Save Changes": function() {
|
||||
save(do_close);
|
||||
}
|
||||
},
|
||||
{
|
||||
"Don't Save": do_close
|
||||
}
|
||||
);
|
||||
} else {
|
||||
window.document.location = configs.page_list_url;
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize galaxy elements.
|
||||
//init_galaxy_elts(editor);
|
||||
|
||||
//
|
||||
// Containers, Galaxy style
|
||||
//
|
||||
var containers_menu = $(
|
||||
"<div class='galaxy-page-editor-button'><a id='insert-galaxy-link' class='action-button popup' href='#'>Paragraph type</a></div>"
|
||||
);
|
||||
$(".wym_area_top").append(containers_menu);
|
||||
|
||||
// Add menu options.
|
||||
var items = {};
|
||||
$.each(editor._options.containersItems, (k, v) => {
|
||||
var tagname = v.name;
|
||||
items[v.title.replace("_", " ")] = () => {
|
||||
editor.container(tagname);
|
||||
};
|
||||
});
|
||||
make_popupmenu(containers_menu, items);
|
||||
|
||||
//
|
||||
// Create 'Insert Link to Galaxy Object' menu.
|
||||
//
|
||||
|
||||
// Add menu button.
|
||||
var insert_link_menu_button = $(
|
||||
"<div><a id='insert-galaxy-link' class='action-button popup' href='#'>Insert Link to Galaxy Object</a></div>"
|
||||
).addClass("galaxy-page-editor-button");
|
||||
$(".wym_area_top").append(insert_link_menu_button);
|
||||
|
||||
// Add menu options.
|
||||
make_popupmenu(insert_link_menu_button, {
|
||||
"Insert History Link": function() {
|
||||
editor.dialog(CONTROLS.DIALOG_HISTORY_LINK);
|
||||
},
|
||||
"Insert Dataset Link": function() {
|
||||
editor.dialog(CONTROLS.DIALOG_DATASET_LINK);
|
||||
},
|
||||
"Insert Workflow Link": function() {
|
||||
editor.dialog(CONTROLS.DIALOG_WORKFLOW_LINK);
|
||||
},
|
||||
"Insert Page Link": function() {
|
||||
editor.dialog(CONTROLS.DIALOG_PAGE_LINK);
|
||||
},
|
||||
"Insert Visualization Link": function() {
|
||||
editor.dialog(CONTROLS.DIALOG_VISUALIZATION_LINK);
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// Create 'Embed Galaxy Object' menu.
|
||||
//
|
||||
|
||||
// Add menu button.
|
||||
var embed_object_button = $(
|
||||
"<div><a id='embed-galaxy-object' class='action-button popup' href='#'>Embed Galaxy Object</a></div>"
|
||||
).addClass("galaxy-page-editor-button");
|
||||
$(".wym_area_top").append(embed_object_button);
|
||||
|
||||
// Add menu options.
|
||||
make_popupmenu(embed_object_button, {
|
||||
"Embed History": function() {
|
||||
editor.dialog(CONTROLS.DIALOG_EMBED_HISTORY);
|
||||
},
|
||||
"Embed Dataset": function() {
|
||||
editor.dialog(CONTROLS.DIALOG_EMBED_DATASET);
|
||||
},
|
||||
"Embed Workflow": function() {
|
||||
editor.dialog(CONTROLS.DIALOG_EMBED_WORKFLOW);
|
||||
},
|
||||
"Embed Visualization": function() {
|
||||
editor.dialog(CONTROLS.DIALOG_EMBED_VISUALIZATION);
|
||||
}
|
||||
//"Embed Page": function() {
|
||||
// editor.dialog(CONTROLS.DIALOG_EMBED_PAGE);
|
||||
//}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,38 +6,38 @@ import _l from "utils/localization";
|
||||
import Modal from "mvc/ui/ui-modal";
|
||||
import { getGalaxyInstance } from "app";
|
||||
|
||||
export var CommunicationServerView = Backbone.View.extend({
|
||||
export const CommunicationServerView = Backbone.View.extend({
|
||||
initialize: function() {
|
||||
this.modal = null;
|
||||
},
|
||||
|
||||
/** makes bootstrap modal and iframe inside it */
|
||||
makeModalIframe: function(e) {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
const Galaxy = getGalaxyInstance();
|
||||
// make modal
|
||||
var host = Galaxy.config.communication_server_host;
|
||||
const host = Galaxy.config.communication_server_host;
|
||||
|
||||
var port = Galaxy.config.communication_server_port;
|
||||
var username = _.escape(Galaxy.user.attributes.username);
|
||||
const port = Galaxy.config.communication_server_port;
|
||||
const username = _.escape(Galaxy.user.attributes.username);
|
||||
|
||||
var persistent_communication_rooms = _.escape(Galaxy.config.persistent_communication_rooms);
|
||||
const persistent_communication_rooms = _.escape(Galaxy.config.persistent_communication_rooms);
|
||||
|
||||
var query_string = `?username=${username}&persistent_communication_rooms=${persistent_communication_rooms}`;
|
||||
const query_string = `?username=${username}&persistent_communication_rooms=${persistent_communication_rooms}`;
|
||||
|
||||
var src = `${host}:${port}${query_string}`;
|
||||
var $el_chat_modal_header = null;
|
||||
var $el_chat_modal_body = null;
|
||||
const src = `${host}:${port}${query_string}`;
|
||||
let $el_chat_modal_header = null;
|
||||
let $el_chat_modal_body = null;
|
||||
|
||||
var iframe_template = `<iframe class="h-100 w-100" src="${src}"> </iframe>`;
|
||||
const iframe_template = `<iframe class="h-100 w-100" src="${src}"> </iframe>`;
|
||||
|
||||
var header_template =
|
||||
const header_template =
|
||||
'<i class="fa fa-comment" aria-hidden="true" title="Communicate with other users"></i>' +
|
||||
'<i class="fa fa-expand expand-compress-modal" aria-hidden="true" title="Maximize"></i>' +
|
||||
'<i class="fa fa-times close-modal" aria-hidden="true" title="Close"></i>';
|
||||
|
||||
var frame_height = 350;
|
||||
var frame_width = 600;
|
||||
var class_names = "ui-modal chat-modal";
|
||||
const frame_height = 350;
|
||||
const frame_width = 600;
|
||||
const class_names = "ui-modal chat-modal";
|
||||
|
||||
// deletes the chat modal if already present and create one
|
||||
if ($(".chat-modal").length > 0) {
|
||||
@@ -92,14 +92,12 @@ export var CommunicationServerView = Backbone.View.extend({
|
||||
|
||||
/**renders the chat icon as a nav item*/
|
||||
render: function() {
|
||||
var self = this;
|
||||
var navItem = {};
|
||||
navItem = {
|
||||
const navItem = {
|
||||
id: "show-chat-online",
|
||||
icon: "fa-comment-o",
|
||||
tooltip: _l("Chat online"),
|
||||
visible: false,
|
||||
onclick: self.makeModalIframe
|
||||
onclick: this.makeModalIframe
|
||||
};
|
||||
return navItem;
|
||||
}
|
||||
|
||||
@@ -10,16 +10,16 @@ export default class Data {
|
||||
* @param {function} callback - Result function called with selection
|
||||
*/
|
||||
dialog(callback, options = {}) {
|
||||
let galaxy = getGalaxyInstance();
|
||||
let host = `${window.location.protocol}//${window.location.hostname}:${window.location.port}`;
|
||||
const galaxy = getGalaxyInstance();
|
||||
const host = `${window.location.protocol}//${window.location.hostname}:${window.location.port}`;
|
||||
Object.assign(options, {
|
||||
callback: callback,
|
||||
history: galaxy.currHistoryPanel && galaxy.currHistoryPanel.model.id,
|
||||
root: galaxy.root,
|
||||
host: host
|
||||
});
|
||||
var instance = Vue.extend(DataDialog);
|
||||
var vm = document.createElement("div");
|
||||
const instance = Vue.extend(DataDialog);
|
||||
const vm = document.createElement("div");
|
||||
$("body").append(vm);
|
||||
new instance({
|
||||
propsData: options
|
||||
@@ -30,8 +30,8 @@ export default class Data {
|
||||
* Creates a history dataset by submitting an upload request
|
||||
*/
|
||||
create(options) {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
let history_panel = Galaxy.currHistoryPanel;
|
||||
const Galaxy = getGalaxyInstance();
|
||||
const history_panel = Galaxy.currHistoryPanel;
|
||||
let history_id = options.history_id;
|
||||
if (!history_id && history_panel) {
|
||||
history_id = history_panel.model.get("id");
|
||||
|
||||
@@ -6,11 +6,11 @@ import QuotaMeter from "mvc/user/user-quotameter";
|
||||
import { getGalaxyInstance } from "app";
|
||||
|
||||
/** Masthead **/
|
||||
var View = Backbone.View.extend({
|
||||
const View = Backbone.View.extend({
|
||||
initialize: function(options) {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
const Galaxy = getGalaxyInstance();
|
||||
|
||||
var self = this;
|
||||
const self = this;
|
||||
this.options = options;
|
||||
this.setElement(this._template());
|
||||
this.$navbarBrandLink = this.$(".navbar-brand");
|
||||
@@ -53,7 +53,7 @@ var View = Backbone.View.extend({
|
||||
// loop through beforeunload functions if the user attempts to unload the page
|
||||
$(window)
|
||||
.on("click", e => {
|
||||
var $download_link = $(e.target).closest("a[download]");
|
||||
const $download_link = $(e.target).closest("a[download]");
|
||||
if ($download_link.length == 1) {
|
||||
if ($("iframe[id=download]").length === 0) {
|
||||
$("body").append(
|
||||
@@ -67,9 +67,9 @@ var View = Backbone.View.extend({
|
||||
}
|
||||
})
|
||||
.on("beforeunload", () => {
|
||||
var text = "";
|
||||
let text = "";
|
||||
self.collection.each(model => {
|
||||
var q = model.get("onbeforeunload") && model.get("onbeforeunload")();
|
||||
const q = model.get("onbeforeunload") && model.get("onbeforeunload")();
|
||||
if (q) {
|
||||
text += `${q} `;
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ import Webhooks from "mvc/webhooks";
|
||||
import Utils from "utils/utils";
|
||||
|
||||
function logoutClick() {
|
||||
let galaxy = getGalaxyInstance();
|
||||
let session_csrf_token = galaxy.session_csrf_token;
|
||||
let url = `${galaxy.root}user/logout?session_csrf_token=${session_csrf_token}`;
|
||||
const galaxy = getGalaxyInstance();
|
||||
const session_csrf_token = galaxy.session_csrf_token;
|
||||
const url = `${galaxy.root}user/logout?session_csrf_token=${session_csrf_token}`;
|
||||
axios.get(url).then(() => {
|
||||
if (galaxy.user) {
|
||||
galaxy.user.clearSessionStorage();
|
||||
@@ -22,7 +22,7 @@ function logoutClick() {
|
||||
});
|
||||
}
|
||||
|
||||
var Collection = Backbone.Collection.extend({
|
||||
const Collection = Backbone.Collection.extend({
|
||||
model: Backbone.Model.extend({
|
||||
defaults: {
|
||||
visible: true,
|
||||
@@ -33,12 +33,12 @@ var Collection = Backbone.Collection.extend({
|
||||
options = options || {};
|
||||
this.reset();
|
||||
|
||||
let Galaxy = getGalaxyInstance();
|
||||
const Galaxy = getGalaxyInstance();
|
||||
|
||||
//
|
||||
// Chat server tab
|
||||
//
|
||||
var extendedNavItem = new CommunicationServerView();
|
||||
const extendedNavItem = new CommunicationServerView();
|
||||
this.add(extendedNavItem.render());
|
||||
|
||||
//
|
||||
@@ -129,9 +129,9 @@ var Collection = Backbone.Collection.extend({
|
||||
callback: function(webhooks) {
|
||||
$(document).ready(() => {
|
||||
webhooks.each(model => {
|
||||
var webhook = model.toJSON();
|
||||
const webhook = model.toJSON();
|
||||
if (webhook.activate) {
|
||||
var obj = {
|
||||
const obj = {
|
||||
id: webhook.id,
|
||||
icon: webhook.config.icon,
|
||||
url: webhook.config.url,
|
||||
@@ -141,7 +141,7 @@ var Collection = Backbone.Collection.extend({
|
||||
};
|
||||
|
||||
// Galaxy.page is undefined for data libraries, workflows pages
|
||||
let Galaxy = getGalaxyInstance();
|
||||
const Galaxy = getGalaxyInstance();
|
||||
if (Galaxy.page) {
|
||||
Galaxy.page.masthead.collection.add(obj);
|
||||
} else if (Galaxy.masthead) {
|
||||
@@ -172,7 +172,7 @@ var Collection = Backbone.Collection.extend({
|
||||
//
|
||||
// Help tab.
|
||||
//
|
||||
var helpTab = {
|
||||
const helpTab = {
|
||||
id: "help",
|
||||
title: _l("Help"),
|
||||
tooltip: _l("Support, contact, and community"),
|
||||
@@ -232,7 +232,7 @@ var Collection = Backbone.Collection.extend({
|
||||
//
|
||||
// User tab.
|
||||
//
|
||||
var userTab = {};
|
||||
let userTab = {};
|
||||
if (!Galaxy.user.id) {
|
||||
if (options.allow_user_creation) {
|
||||
userTab = {
|
||||
@@ -240,7 +240,7 @@ var Collection = Backbone.Collection.extend({
|
||||
title: _l("Login or Register"),
|
||||
cls: "loggedout-only",
|
||||
url: "login",
|
||||
tooltip: _l("Account registration or login")
|
||||
tooltip: _l("Log in or register a new account")
|
||||
};
|
||||
} else {
|
||||
userTab = {
|
||||
@@ -248,7 +248,7 @@ var Collection = Backbone.Collection.extend({
|
||||
title: _l("Login"),
|
||||
cls: "loggedout-only",
|
||||
tooltip: _l("Login"),
|
||||
url: "user/login",
|
||||
url: "login",
|
||||
target: "galaxy_main",
|
||||
noscratchbook: true
|
||||
};
|
||||
@@ -279,22 +279,27 @@ var Collection = Backbone.Collection.extend({
|
||||
onclick: logoutClick
|
||||
},
|
||||
{
|
||||
title: _l("Saved Datasets"),
|
||||
title: _l("Datasets"),
|
||||
url: "datasets/list",
|
||||
target: "__use_router__"
|
||||
},
|
||||
{
|
||||
title: _l("Saved Histories"),
|
||||
title: _l("Histories"),
|
||||
url: "histories/list",
|
||||
target: "__use_router__"
|
||||
},
|
||||
{
|
||||
title: _l("Saved Pages"),
|
||||
title: _l("Histories shared with me"),
|
||||
url: "histories/list_shared",
|
||||
target: "__use_router__"
|
||||
},
|
||||
{
|
||||
title: _l("Pages"),
|
||||
url: "pages/list",
|
||||
target: "__use_router__"
|
||||
},
|
||||
{
|
||||
title: _l("Saved Visualizations"),
|
||||
title: _l("Visualizations"),
|
||||
url: "visualizations/list",
|
||||
target: "__use_router__"
|
||||
}
|
||||
@@ -307,7 +312,7 @@ var Collection = Backbone.Collection.extend({
|
||||
});
|
||||
|
||||
/** Masthead tab **/
|
||||
var Tab = Backbone.View.extend({
|
||||
const Tab = Backbone.View.extend({
|
||||
initialize: function(options) {
|
||||
this.model = options.model;
|
||||
this.setElement(this._template());
|
||||
@@ -403,7 +408,7 @@ var Tab = Backbone.View.extend({
|
||||
if (options.onclick) {
|
||||
options.onclick();
|
||||
} else {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
const Galaxy = getGalaxyInstance();
|
||||
if (options.target == "__use_router__" && typeof Galaxy.page != "undefined") {
|
||||
Galaxy.page.router.executeUseRouter(options.url);
|
||||
} else {
|
||||
@@ -429,7 +434,7 @@ var Tab = Backbone.View.extend({
|
||||
|
||||
/** Handle click event */
|
||||
_toggleClick: function(e) {
|
||||
var model = this.model;
|
||||
const model = this.model;
|
||||
e.preventDefault();
|
||||
$(".tooltip").hide();
|
||||
model.trigger("dispatch", m => {
|
||||
@@ -442,7 +447,7 @@ var Tab = Backbone.View.extend({
|
||||
if (model.get("onclick")) {
|
||||
model.get("onclick")();
|
||||
} else {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
const Galaxy = getGalaxyInstance();
|
||||
if (model.attributes.target == "__use_router__" && typeof Galaxy.page != "undefined") {
|
||||
Galaxy.page.router.executeUseRouter(model.attributes.url);
|
||||
} else {
|
||||
|
||||
@@ -28,7 +28,7 @@ $.extend(Modal.prototype, {
|
||||
}
|
||||
// Buttons
|
||||
this.$footer.hide();
|
||||
var $buttons = this.$footer.find(".buttons").html("");
|
||||
const $buttons = this.$footer.find(".buttons").html("");
|
||||
if (options.buttons) {
|
||||
$.each(options.buttons, (name, value) => {
|
||||
$buttons
|
||||
@@ -41,7 +41,7 @@ $.extend(Modal.prototype, {
|
||||
});
|
||||
this.$footer.show();
|
||||
}
|
||||
var $extraButtons = this.$footer.find(".extra_buttons").html("");
|
||||
const $extraButtons = this.$footer.find(".extra_buttons").html("");
|
||||
if (options.extra_buttons) {
|
||||
$.each(options.extra_buttons, (name, value) => {
|
||||
$extraButtons
|
||||
@@ -55,7 +55,7 @@ $.extend(Modal.prototype, {
|
||||
this.$footer.show();
|
||||
}
|
||||
// Body
|
||||
var body = options.body;
|
||||
let body = options.body;
|
||||
if (body == "progress") {
|
||||
body = $(
|
||||
"<div class='progress progress-striped active'><div class='progress-bar' style='width: 100%'></div></div>"
|
||||
@@ -83,7 +83,7 @@ $.extend(Modal.prototype, {
|
||||
}
|
||||
},
|
||||
hide: function() {
|
||||
var modal = this;
|
||||
const modal = this;
|
||||
modal.$dialog.fadeOut(() => {
|
||||
modal.$overlay.hide();
|
||||
modal.$backdrop.removeClass("in");
|
||||
@@ -94,7 +94,7 @@ $.extend(Modal.prototype, {
|
||||
}
|
||||
});
|
||||
|
||||
var modal;
|
||||
let modal;
|
||||
|
||||
// TODO: move into init chain
|
||||
$(() => {
|
||||
@@ -131,9 +131,9 @@ export function show_message(title, body, buttons, extra_buttons, init_fn) {
|
||||
}
|
||||
|
||||
export function show_in_overlay(options) {
|
||||
var width = options.width || "600";
|
||||
var height = options.height || "400";
|
||||
var scroll = options.scroll || "auto";
|
||||
const width = options.width || "600";
|
||||
const height = options.height || "400";
|
||||
const scroll = options.scroll || "auto";
|
||||
$("#overlay-background").bind("click.overlay", () => {
|
||||
hide_modal();
|
||||
$("#overlay-background").unbind("click.overlay");
|
||||
|
||||
@@ -9,13 +9,13 @@ import Panel from "layout/panel";
|
||||
import Modal from "mvc/ui/ui-modal";
|
||||
import Utils from "utils/utils";
|
||||
|
||||
var View = Backbone.View.extend({
|
||||
const View = Backbone.View.extend({
|
||||
el: "body",
|
||||
className: "full-content",
|
||||
_panelids: ["left", "right"],
|
||||
|
||||
initialize: function(options) {
|
||||
var self = this;
|
||||
const self = this;
|
||||
this.config = _.defaults(options.config || {}, {
|
||||
message_box_visible: false,
|
||||
message_box_content: "",
|
||||
@@ -27,7 +27,7 @@ var View = Backbone.View.extend({
|
||||
});
|
||||
|
||||
// attach global objects, build mastheads
|
||||
let Galaxy = getGalaxyInstance();
|
||||
const Galaxy = getGalaxyInstance();
|
||||
Galaxy.modal = this.modal = new Modal.View();
|
||||
Galaxy.router = this.router = options.Router && new options.Router(self, options);
|
||||
Galaxy.data = this.data = new Data(this);
|
||||
@@ -72,11 +72,11 @@ var View = Backbone.View.extend({
|
||||
this.panels = {};
|
||||
if (!this.config.hide_panels) {
|
||||
_.each(this._panelids, panel_id => {
|
||||
var panel_class_name = panel_id.charAt(0).toUpperCase() + panel_id.slice(1);
|
||||
var panel_class = options[panel_class_name];
|
||||
const panel_class_name = panel_id.charAt(0).toUpperCase() + panel_id.slice(1);
|
||||
const panel_class = options[panel_class_name];
|
||||
if (panel_class) {
|
||||
var panel_instance = new panel_class(self, options);
|
||||
var panel_el = self.$(`#${panel_id}`);
|
||||
const panel_instance = new panel_class(self, options);
|
||||
const panel_el = self.$(`#${panel_id}`);
|
||||
self[panel_instance.toString()] = panel_instance;
|
||||
self.panels[panel_id] = new Panel.SidePanel({
|
||||
id: panel_id,
|
||||
@@ -116,8 +116,8 @@ var View = Backbone.View.extend({
|
||||
/** Render message box */
|
||||
renderMessageBox: function() {
|
||||
if (this.config.message_box_visible) {
|
||||
var content = this.config.message_box_content || "";
|
||||
var level = this.config.message_box_class || "info";
|
||||
const content = this.config.message_box_content || "";
|
||||
const level = this.config.message_box_class || "info";
|
||||
this.$el.addClass("has-message-box");
|
||||
this.$messagebox
|
||||
.attr("class", `panel-${level}-message`)
|
||||
@@ -134,8 +134,8 @@ var View = Backbone.View.extend({
|
||||
/** Render inactivity warning */
|
||||
renderInactivityBox: function() {
|
||||
if (this.config.show_inactivity_warning) {
|
||||
var content = this.config.inactivity_box_content || "";
|
||||
var verificationLink = $("<a/>")
|
||||
const content = this.config.inactivity_box_content || "";
|
||||
const verificationLink = $("<a/>")
|
||||
.attr("href", `${getAppRoot()}user/resend_verification`)
|
||||
.text("Resend verification");
|
||||
this.$el.addClass("has-inactivity-box");
|
||||
@@ -153,9 +153,9 @@ var View = Backbone.View.extend({
|
||||
|
||||
/** Render panels */
|
||||
renderPanels: function() {
|
||||
var self = this;
|
||||
const self = this;
|
||||
_.each(this._panelids, panel_id => {
|
||||
var panel = self.panels[panel_id];
|
||||
const panel = self.panels[panel_id];
|
||||
if (panel) {
|
||||
panel.render();
|
||||
} else {
|
||||
@@ -190,11 +190,11 @@ var View = Backbone.View.extend({
|
||||
|
||||
/** Check if the communication server is online and show the icon otherwise hide the icon */
|
||||
_checkCommunicationServerOnline: function() {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
var host = Galaxy.config.communication_server_host;
|
||||
var port = Galaxy.config.communication_server_port;
|
||||
var preferences = Galaxy.user.attributes.preferences;
|
||||
var $chat_icon_element = $("#show-chat-online");
|
||||
const Galaxy = getGalaxyInstance();
|
||||
const host = Galaxy.config.communication_server_host;
|
||||
const port = Galaxy.config.communication_server_port;
|
||||
const preferences = Galaxy.user.attributes.preferences;
|
||||
const $chat_icon_element = $("#show-chat-online");
|
||||
/** Check if the user has deactivated the communication in it's personal settings */
|
||||
if (preferences && ["1", "true"].indexOf(preferences.communication_server) != -1) {
|
||||
// See if the configured communication server is available
|
||||
|
||||
@@ -3,11 +3,11 @@ import _ from "underscore";
|
||||
import Backbone from "backbone";
|
||||
import { getGalaxyInstance } from "app";
|
||||
|
||||
var MIN_PANEL_WIDTH = 160;
|
||||
var MAX_PANEL_WIDTH = 800;
|
||||
const MIN_PANEL_WIDTH = 160;
|
||||
const MAX_PANEL_WIDTH = 800;
|
||||
|
||||
/** View for left/right panels used by Page view */
|
||||
var SidePanel = Backbone.View.extend({
|
||||
const SidePanel = Backbone.View.extend({
|
||||
initialize: function(options) {
|
||||
this.view = options.view;
|
||||
this.hidden = false;
|
||||
@@ -24,9 +24,9 @@ var SidePanel = Backbone.View.extend({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
var panel = this.view;
|
||||
var components = this.view.model.attributes || {};
|
||||
const self = this;
|
||||
const panel = this.view;
|
||||
const components = this.view.model.attributes || {};
|
||||
this.$el.html(this._templatePanel(this.id));
|
||||
_.each(components.buttons, button => {
|
||||
self.$(".panel-header-buttons").append(button.$el);
|
||||
@@ -76,16 +76,16 @@ var SidePanel = Backbone.View.extend({
|
||||
},
|
||||
|
||||
_mousedownDragHandler: function(ev) {
|
||||
var self = this;
|
||||
var draggingLeft = this.id === "left";
|
||||
const self = this;
|
||||
const draggingLeft = this.id === "left";
|
||||
// Save the mouse position and width of the element (panel) when the
|
||||
// drag interaction is first started
|
||||
var initialX = ev.pageX;
|
||||
var initialWidth = self.$el.width();
|
||||
const initialX = ev.pageX;
|
||||
const initialWidth = self.$el.width();
|
||||
|
||||
function move(e) {
|
||||
var delta = e.pageX - initialX;
|
||||
var newWidth = draggingLeft ? initialWidth + delta : initialWidth - delta;
|
||||
const delta = e.pageX - initialX;
|
||||
let newWidth = draggingLeft ? initialWidth + delta : initialWidth - delta;
|
||||
// Limit range
|
||||
newWidth = Math.min(MAX_PANEL_WIDTH, Math.max(MIN_PANEL_WIDTH, newWidth));
|
||||
self.resize(newWidth);
|
||||
@@ -113,36 +113,36 @@ var SidePanel = Backbone.View.extend({
|
||||
},
|
||||
|
||||
show: function() {
|
||||
if (!this.hidden) {
|
||||
return;
|
||||
if (this.hidden && !this.motionQueue) {
|
||||
this.motionQueue = 2;
|
||||
const animation = {};
|
||||
const whichSide = this.id;
|
||||
animation[whichSide] = 0;
|
||||
this.$el
|
||||
.css(whichSide, -this.saved_size)
|
||||
.css("width", this.saved_size)
|
||||
.animate(animation, "fast", () => this.motionQueue--);
|
||||
animation[whichSide] = this.saved_size;
|
||||
this.$center().animate(animation, "fast", () => this.motionQueue--);
|
||||
this.hidden = false;
|
||||
this.$toggleButton().removeClass("hidden");
|
||||
}
|
||||
var self = this;
|
||||
var animation = {};
|
||||
var whichSide = this.id;
|
||||
animation[whichSide] = 0;
|
||||
self.$el
|
||||
.css(whichSide, -this.saved_size)
|
||||
.show()
|
||||
.animate(animation, "fast", () => {
|
||||
self.resize(self.saved_size);
|
||||
});
|
||||
self.hidden = false;
|
||||
self.$toggleButton().removeClass("hidden");
|
||||
return this;
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
if (this.hidden) {
|
||||
return;
|
||||
if (!this.hidden && !this.motionQueue) {
|
||||
this.motionQueue = 2;
|
||||
const animation = {};
|
||||
const whichSide = this.id;
|
||||
this.saved_size = this.$el.width();
|
||||
animation[whichSide] = -this.saved_size;
|
||||
this.$el.animate(animation, "fast", () => this.motionQueue--);
|
||||
animation[whichSide] = 0;
|
||||
this.$center().animate(animation, "fast", () => this.motionQueue--);
|
||||
this.hidden = true;
|
||||
this.$toggleButton().addClass("hidden");
|
||||
}
|
||||
var animation = {};
|
||||
var whichSide = this.id;
|
||||
this.saved_size = this.$el.width();
|
||||
animation[whichSide] = -this.saved_size;
|
||||
this.$el.animate(animation, "fast");
|
||||
this.$center().css(whichSide, 0);
|
||||
this.hidden = true;
|
||||
this.$toggleButton().addClass("hidden");
|
||||
return this;
|
||||
},
|
||||
|
||||
@@ -156,7 +156,7 @@ var SidePanel = Backbone.View.extend({
|
||||
//TODO: only used in message.mako?
|
||||
/** */
|
||||
handle_minwidth_hint: function(hint) {
|
||||
var space = this.$center().width() - (this.hidden ? this.saved_size : 0);
|
||||
const space = this.$center().width() - (this.hidden ? this.saved_size : 0);
|
||||
if (space < hint) {
|
||||
if (!this.hidden) {
|
||||
this.toggle();
|
||||
@@ -189,16 +189,16 @@ var SidePanel = Backbone.View.extend({
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// TODO: side should be defined by page - not here
|
||||
var LeftPanel = SidePanel.extend({
|
||||
const LeftPanel = SidePanel.extend({
|
||||
id: "left"
|
||||
});
|
||||
|
||||
var RightPanel = SidePanel.extend({
|
||||
const RightPanel = SidePanel.extend({
|
||||
id: "right"
|
||||
});
|
||||
|
||||
/** Center panel with the ability to switch between iframe and view */
|
||||
var CenterPanel = Backbone.View.extend({
|
||||
const CenterPanel = Backbone.View.extend({
|
||||
initialize: function(options) {
|
||||
this.setElement($(this.template()));
|
||||
this.$frame = this.$(".center-frame");
|
||||
@@ -208,9 +208,9 @@ var CenterPanel = Backbone.View.extend({
|
||||
|
||||
/** Display iframe if its target url changes, hide center panel */
|
||||
_iframeChangeHandler: function(ev) {
|
||||
var iframe = ev.currentTarget;
|
||||
var location = iframe.contentWindow && iframe.contentWindow.location;
|
||||
var Galaxy = getGalaxyInstance();
|
||||
const iframe = ev.currentTarget;
|
||||
const location = iframe.contentWindow && iframe.contentWindow.location;
|
||||
const Galaxy = getGalaxyInstance();
|
||||
// Adding try/catch to manage a CORS error in toolshed. Accessing
|
||||
// location.host is a CORS no-no
|
||||
try {
|
||||
@@ -231,7 +231,7 @@ var CenterPanel = Backbone.View.extend({
|
||||
|
||||
/** Display a view in the center panel, hide iframe */
|
||||
display: function(view) {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
const Galaxy = getGalaxyInstance();
|
||||
let contentWindow = this.$frame[0].contentWindow || {};
|
||||
let message;
|
||||
try {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getGalaxyInstance } from "app";
|
||||
import QUERY_STRING from "utils/query-string-parsing";
|
||||
import Ui from "mvc/ui/ui-misc";
|
||||
|
||||
var Router = Backbone.Router.extend({
|
||||
const Router = Backbone.Router.extend({
|
||||
// TODO: not many client routes at this point - fill and remove from server.
|
||||
// since we're at root here, this may be the last to be routed entirely on the client.
|
||||
initialize: function(page, options) {
|
||||
@@ -14,7 +14,7 @@ var Router = Backbone.Router.extend({
|
||||
},
|
||||
|
||||
executeUseRouter: function(url) {
|
||||
let prefix = getAppRoot();
|
||||
const prefix = getAppRoot();
|
||||
if (url.startsWith(prefix)) {
|
||||
url = url.replace(prefix, "/");
|
||||
}
|
||||
@@ -28,9 +28,9 @@ var Router = Backbone.Router.extend({
|
||||
.toString(36)
|
||||
.substr(2);
|
||||
url += url.indexOf("?") == -1 ? "?" : "&";
|
||||
let bustParam = $.param(data, true);
|
||||
const bustParam = $.param(data, true);
|
||||
url += bustParam;
|
||||
let Galaxy = getGalaxyInstance();
|
||||
const Galaxy = getGalaxyInstance();
|
||||
Galaxy.params = data;
|
||||
this.navigate(url, { trigger: true });
|
||||
window.history.replaceState(window.history.state, "", window.location.pathname.replace(bustParam, ""));
|
||||
@@ -38,9 +38,9 @@ var Router = Backbone.Router.extend({
|
||||
|
||||
/** override to parse query string into obj and send to each route */
|
||||
execute: function(callback, args, name) {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
const Galaxy = getGalaxyInstance();
|
||||
Galaxy.debug("router execute:", callback, args, name);
|
||||
var queryObj = QUERY_STRING.parse(args.pop());
|
||||
const queryObj = QUERY_STRING.parse(args.pop());
|
||||
args.push(queryObj);
|
||||
if (callback) {
|
||||
if (this.authenticate(args, name)) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user