Merge branch 'dev' into activate_toolshed

This commit is contained in:
guerler
2019-07-05 14:09:08 +02:00
876 changed files with 18873 additions and 9206 deletions
+11 -1
View File
@@ -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
View File
@@ -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.
+1 -28
View File
@@ -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();
+74 -88
View File
@@ -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})`;
};
+3 -3
View File
@@ -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;
}
+9 -3
View File
@@ -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";
+20 -16
View File
@@ -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 {
+1 -1
View File
@@ -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 })
+14 -11
View File
@@ -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)));
};
+11 -13
View File
@@ -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
};
@@ -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>&lt;YOUR MESSAGE&gt;</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}`,
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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
};
+1 -1
View File
@@ -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;
+25 -675
View File
@@ -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;
}
+6 -6
View File
@@ -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 -6
View File
@@ -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} `;
}
+26 -21
View File
@@ -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 {
+8 -8
View File
@@ -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");
+18 -18
View File
@@ -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
+44 -44
View File
@@ -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 {
+6 -6
View File
@@ -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