diff --git a/client/package.json b/client/package.json index 04561397485..44eaae21e1a 100644 --- a/client/package.json +++ b/client/package.json @@ -82,7 +82,8 @@ "vueisotope": "^3.1.2", "vuex": "^3.4.0", "vuex-cache": "^3.2.0", - "vuex-persist": "^3.1.3" + "vuex-persist": "^3.1.3", + "vue-observe-visibility": "^1.0.0" }, "scripts": { "watch": "gulp && yarn run save-build-hash && yarn run webpack-watch", diff --git a/client/src/components/LibraryFolder/LibraryFolder.vue b/client/src/components/LibraryFolder/LibraryFolder.vue index b56a83cf47d..10248d84889 100644 --- a/client/src/components/LibraryFolder/LibraryFolder.vue +++ b/client/src/components/LibraryFolder/LibraryFolder.vue @@ -216,7 +216,7 @@ +
+
+
+ + 1 + +
+
{{ folder.name }}
+
+
+
+
+ + You are logged in as an administrator therefore you can manage any folder on + this Galaxy instance. Please make sure you understand the consequences. + + + You can assign any number of roles to any of the following permission types. However please read + carefully the implications of such actions. + +
+
+
+ +
+

Folder permissions

+

+ Roles that can manage permissions on this folder +

+ + + + + + User with any of these roles can manage permissions on this folder. + + + +

+ Roles that can add items to this folder +

+ + + + + + User with any of these roles can add items to this folder (folders and + datasets). + + + +

+ Roles that can modify this folder +

+ + + + + + User with any of these roles can modify this folder (name, etc.). + + + +
+
+
+
+ + + + + diff --git a/client/src/components/LibraryFolder/LibraryFolderPermissions/PermissionsInputField.vue b/client/src/components/LibraryFolder/LibraryFolderPermissions/PermissionsInputField.vue new file mode 100644 index 00000000000..3a889956ebc --- /dev/null +++ b/client/src/components/LibraryFolder/LibraryFolderPermissions/PermissionsInputField.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/client/src/components/LibraryFolder/LibraryFolderPermissions/services.js b/client/src/components/LibraryFolder/LibraryFolderPermissions/services.js new file mode 100644 index 00000000000..a7781482066 --- /dev/null +++ b/client/src/components/LibraryFolder/LibraryFolderPermissions/services.js @@ -0,0 +1,66 @@ +import axios from "axios"; +import { rethrowSimple } from "utils/simple-error"; +import { getAppRoot } from "onload/loadConfig"; + +export class Services { + constructor(options = {}) { + this.root = options.root || getAppRoot(); + } + + async getFolderPermissions(id) { + const url = `${this.root}api/folders/${id}/permissions?scope=current`; + try { + const response = await axios.get(url); + return response.data; + } catch (e) { + rethrowSimple(e); + } + } + async getFolder(id) { + const url = `${this.root}api/folders/${id}`; + try { + const response = await axios.get(url); + return response.data; + } catch (e) { + rethrowSimple(e); + } + } + async getSelectOptions(id, is_library_access, page, page_limit, searchQuery) { + searchQuery = searchQuery ? `&q=${searchQuery}` : ""; + const url = `${this.root}api/folders/${id}/permissions?scope=available&is_library_access=${is_library_access}&page_limit=${page_limit}&page=${page}${searchQuery}`; + try { + const response = await axios.get(url); + return response.data; + } catch (e) { + rethrowSimple(e); + } + } + async setPermissions(id, add_ids, manage_ids, modify_ids, onSuccess, onError) { + var formData = new FormData(); + formData.append( + "add_ids[]", + add_ids.map((a) => a.id) + ); + formData.append( + "manage_ids[]", + manage_ids.map((a) => a.id) + ); + formData.append( + "modify_ids[]", + modify_ids.map((a) => a.id) + ); + + axios({ + method: "post", + url: `${getAppRoot()}api/folders/${id}/permissions?action=set_permissions`, + data: formData, + headers: { "Content-Type": "multipart/form-data" }, + }) + .then(function (response) { + onSuccess(response); + }) + .catch((response) => { + onError(response); + }); + } +} diff --git a/client/src/components/LibraryFolder/LibraryFolderRouter.vue b/client/src/components/LibraryFolder/LibraryFolderRouter.vue new file mode 100644 index 00000000000..7de34fcdf67 --- /dev/null +++ b/client/src/components/LibraryFolder/LibraryFolderRouter.vue @@ -0,0 +1,28 @@ + diff --git a/client/src/components/LibraryFolder/icons.js b/client/src/components/LibraryFolder/icons.js index cf35b81b4c0..3255459ffe5 100644 --- a/client/src/components/LibraryFolder/icons.js +++ b/client/src/components/LibraryFolder/icons.js @@ -16,6 +16,7 @@ import { faDownload, faBook, faMinusSquare, + faAngleDoubleLeft, } from "@fortawesome/free-solid-svg-icons"; import { library } from "@fortawesome/fontawesome-svg-core"; @@ -36,6 +37,8 @@ const tableIcons = [ faSquare, faMinusSquare, ]; + +const manageIcons = [faAngleDoubleLeft, faSave]; const topBarIcons = [faHome, faPlus, faInfoCircle, faTrash, faDownload, faBook]; export function initFolderTableIcons() { @@ -44,6 +47,12 @@ export function initFolderTableIcons() { }); } +export function initManageFolderIcons() { + manageIcons.forEach((icon) => { + library.add(icon); + }); +} + export function initTopBarIcons() { topBarIcons.forEach((icon) => { library.add(icon); diff --git a/client/src/entry/analysis/AnalysisRouter.js b/client/src/entry/analysis/AnalysisRouter.js index 57b307aecb4..f38c044cdef 100644 --- a/client/src/entry/analysis/AnalysisRouter.js +++ b/client/src/entry/analysis/AnalysisRouter.js @@ -29,7 +29,6 @@ import WorkflowImport from "components/Workflow/WorkflowImport.vue"; import TrsImport from "components/Workflow/TrsImport.vue"; import TrsSearch from "components/Workflow/TrsSearch.vue"; import InteractiveTools from "components/InteractiveTools/InteractiveTools.vue"; -import LibraryFolder from "components/LibraryFolder/LibraryFolder.vue"; import WorkflowList from "components/Workflow/WorkflowList.vue"; import HistoryImport from "components/HistoryImport.vue"; import { HistoryExport } from "components/HistoryExport/index"; @@ -49,12 +48,14 @@ import DisplayStructure from "components/DisplayStructured.vue"; import { CloudAuth } from "components/User/CloudAuth"; import { ExternalIdentities } from "components/User/ExternalIdentities"; import Confirmation from "components/login/Confirmation.vue"; +import LibraryFolderRouter from "components/LibraryFolder/LibraryFolderRouter.vue"; import Vue from "vue"; import store from "store"; +import VueRouterMain from "./VueRouterMain.vue"; /** Routes */ -export const getAnalysisRouter = (Galaxy) => - Router.extend({ +export const getAnalysisRouter = (Galaxy) => { + return Router.extend({ routes: { "(/)(#)(_=_)": "home", "(/)root*": "home", @@ -99,6 +100,7 @@ export const getAnalysisRouter = (Galaxy) => "(/)datasets/error": "show_dataset_error", "(/)interactivetool_entry_points(/)list": "show_interactivetool_list", "(/)library/folders(/)(:folder_id)": "show_library_folder", + "(/)library/folders/permissions(/)(:folder_id)": "show_library_folder", }, require_login: ["show_user", "show_user_form", "show_workflows", "show_cloud_auth", "show_external_ids"], @@ -117,6 +119,17 @@ export const getAnalysisRouter = (Galaxy) => this.page.display(container, noPadding); new instance({ store, propsData }).$mount(container); }, + _display_vue_router: function (router, propsData = {}, active_tab = null, noPadding = false) { + const container = document.createElement("div"); + if (active_tab) { + container.active_tab = active_tab; + } + this.page.display(container, noPadding); + new Vue({ + router: router, + render: (h) => h(VueRouterMain), + }).$mount(container); + }, show_tours: function (tour_id) { if (tour_id) { @@ -152,7 +165,7 @@ export const getAnalysisRouter = (Galaxy) => show_library_folder: function (folder_id) { this.page.toolPanel?.component.hide(0); this.page.panels.right.hide(); - this._display_vue_helper(LibraryFolder, { folder_id: folder_id }); + this._display_vue_router(LibraryFolderRouter, { folder_id: folder_id }); }, show_cloud_auth: function () { @@ -457,3 +470,4 @@ export const getAnalysisRouter = (Galaxy) => this._display_vue_helper(WorkflowRun, props, "workflow"); }, }); +}; diff --git a/client/src/entry/analysis/VueRouterMain.vue b/client/src/entry/analysis/VueRouterMain.vue new file mode 100644 index 00000000000..5e1bb628ef0 --- /dev/null +++ b/client/src/entry/analysis/VueRouterMain.vue @@ -0,0 +1,3 @@ + diff --git a/client/src/galaxy.library.js b/client/src/galaxy.library.js index 254abe829c0..45cbb3ad954 100644 --- a/client/src/galaxy.library.js +++ b/client/src/galaxy.library.js @@ -13,7 +13,6 @@ import mod_librarylist_view from "mvc/library/library-librarylist-view"; import mod_librarytoolbar_view from "mvc/library/library-librarytoolbar-view"; import mod_library_dataset_view from "mvc/library/library-dataset-view"; import mod_library_library_view from "mvc/library/library-library-view"; -import mod_library_folder_view from "mvc/library/library-folder-view"; // ============================================================================ /** * The Data Libraries router. Takes care about triggering routes @@ -188,16 +187,6 @@ var GalaxyLibrary = Backbone.View.extend({ }); }); - this.library_router.on("route:folder_permissions", (folder_id) => { - if (Galaxy.libraries.folderView) { - Galaxy.libraries.folderView.$el.unbind("click"); - } - Galaxy.libraries.folderView = new mod_library_folder_view.FolderView({ - id: folder_id, - show_permissions: true, - }); - }); - Backbone.history.start({ pushState: false }); }, }); diff --git a/client/src/mvc/library/library-folder-view.js b/client/src/mvc/library/library-folder-view.js deleted file mode 100644 index 2a9c8719504..00000000000 --- a/client/src/mvc/library/library-folder-view.js +++ /dev/null @@ -1,263 +0,0 @@ -import _ from "underscore"; -import $ from "jquery"; -import Backbone from "backbone"; -import { getGalaxyInstance } from "app"; -import { getAppRoot } from "onload/loadConfig"; -import { Toast } from "ui/toast"; -import mod_library_model from "mvc/library/library-model"; -import mod_select from "mvc/ui/ui-select"; - -var FolderView = Backbone.View.extend({ - el: "#center", - - model: null, - - options: {}, - - events: { - "click .toolbtn_save_permissions": "savePermissions", - }, - - initialize: function (options) { - this.options = _.extend(this.options, options); - if (this.options.id) { - this.fetchFolder(); - } - }, - - fetchFolder: function (options) { - const Galaxy = getGalaxyInstance(); - this.options = _.extend(this.options, options); - this.model = new mod_library_model.FolderAsModel({ - id: this.options.id, - }); - var that = this; - this.model.fetch({ - success: function () { - if (that.options.show_permissions) { - that.showPermissions(); - } - }, - error: function (model, response) { - if (typeof response.responseJSON !== "undefined") { - Toast.error(`${response.responseJSON.err_msg} Click this to go back.`, "", { - onclick: function () { - Galaxy.libraries.library_router.back(); - }, - }); - } else { - Toast.error("An error occurred. Click this to go back.", "", { - onclick: function () { - Galaxy.libraries.library_router.back(); - }, - }); - } - }, - }); - }, - - showPermissions: function (options) { - const Galaxy = getGalaxyInstance(); - this.options = _.extend(this.options, options); - $(".tooltip").remove(); - - var is_admin = false; - if (Galaxy.user) { - is_admin = Galaxy.user.isAdmin(); - } - var template = this.templateFolderPermissions(); - this.$el.html(template({ folder: this.model, is_admin: is_admin, root_path: getAppRoot() })); - - var self = this; - $.get(`${getAppRoot()}api/folders/${self.id}/permissions?scope=current`) - .done((fetched_permissions) => { - self.prepareSelectBoxes({ - fetched_permissions: fetched_permissions, - }); - }) - .fail(() => { - Toast.error("An error occurred while attempting to fetch folder permissions."); - }); - - $('#center [data-toggle="tooltip"]').tooltip({ trigger: "hover" }); - //hack to show scrollbars - $("#center").css("overflow", "auto"); - }, - - _serializeRoles: function (role_list) { - var selected_roles = []; - for (var i = 0; i < role_list.length; i++) { - selected_roles.push(`${role_list[i][1]}:${role_list[i][0]}`); - } - return selected_roles; - }, - - prepareSelectBoxes: function (options) { - this.options = _.extend(this.options, options); - var fetched_permissions = this.options.fetched_permissions; - var self = this; - - var selected_add_item_roles = this._serializeRoles(fetched_permissions.add_library_item_role_list); - var selected_manage_folder_roles = this._serializeRoles(fetched_permissions.manage_folder_role_list); - var selected_modify_folder_roles = this._serializeRoles(fetched_permissions.modify_folder_role_list); - - self.addSelectObject = new mod_select.View( - this._createSelectOptions(this, "add_perm", selected_add_item_roles, false) - ); - self.manageSelectObject = new mod_select.View( - this._createSelectOptions(this, "manage_perm", selected_manage_folder_roles, false) - ); - self.modifySelectObject = new mod_select.View( - this._createSelectOptions(this, "modify_perm", selected_modify_folder_roles, false) - ); - }, - - _createSelectOptions: function (self, id, init_data) { - var select_options = { - minimumInputLength: 0, - css: id, - multiple: true, - placeholder: "Click to select a role", - container: self.$el.find(`#${id}`), - ajax: { - url: `${getAppRoot()}api/folders/${self.id}/permissions?scope=available`, - dataType: "json", - quietMillis: 100, - data: function (term, page) { - // page is the one-based page number tracked by Select2 - return { - q: term, //search term - page_limit: 10, // page size - page: page, // page number - }; - }, - results: function (data, page) { - var more = page * 10 < data.total; // whether or not there are more results available - // notice we return the value of more so Select2 knows if more results can be loaded - return { results: data.roles, more: more }; - }, - }, - formatResult: function roleFormatResult(role) { - return `${role.name} type: ${role.type}`; - }, - - formatSelection: function roleFormatSelection(role) { - return role.name; - }, - initSelection: function (element, callback) { - // the input tag has a value attribute preloaded that points to a preselected role's id - // this function resolves that id attribute to an object that select2 can render - // using its formatResult renderer - that way the role name is shown preselected - var data = []; - $(element.val().split(",")).each(function () { - var item = this.split(":"); - data.push({ - id: item[0], - name: item[1], - }); - }); - callback(data); - }, - initialData: init_data.join(","), - dropdownCssClass: "bigdrop", // apply css that makes the dropdown taller - }; - - return select_options; - }, - - /** - * Extract the role ids from Select2 elements's 'data' - */ - _extractIds: function (roles_list) { - var ids_list = []; - for (var i = roles_list.length - 1; i >= 0; i--) { - ids_list.push(roles_list[i].id); - } - return ids_list; - }, - - /** - * Save the permissions for roles entered in the select boxes. - */ - savePermissions: function (event) { - var self = this; - var add_ids = this._extractIds(this.addSelectObject.$el.select2("data")); - var manage_ids = this._extractIds(this.manageSelectObject.$el.select2("data")); - var modify_ids = this._extractIds(this.modifySelectObject.$el.select2("data")); - $.post(`${getAppRoot()}api/folders/${self.id}/permissions?action=set_permissions`, { - "add_ids[]": add_ids, - "manage_ids[]": manage_ids, - "modify_ids[]": modify_ids, - }) - .done((fetched_permissions) => { - self.showPermissions({ - fetched_permissions: fetched_permissions, - }); - Toast.success("Permissions saved."); - }) - .fail(() => { - Toast.error("An error occurred while attempting to set folder permissions."); - }); - }, - - templateFolderPermissions: function () { - return _.template( - `
-
- "> - - -
-

- Folder: <%= _.escape(folder.get("name")) %> -

-
- <% if (is_admin) { %> - You are logged in as an administrator therefore you can manage any - folder on this Galaxy instance. Please make sure you understand the consequences. - <% } else { %> - You can assign any number of roles to any of the following permission types. - However please read carefully the implications of such actions. - <% }%> -
-
-

Folder permissions

-

- Roles that can manage permissions on this folder -

-
-
- User with any of these roles can manage permissions on this folder. -
-

- Roles that can add items to this folder -

-
-
- User with any of these roles can add items to this folder (folders and datasets). -
-

- Roles that can modify this folder -

-
-
- User with any of these roles can modify this folder (name, etc.). -
- -
-
` - ); - }, -}); - -export default { - FolderView: FolderView, -}; diff --git a/client/yarn.lock b/client/yarn.lock index b013e24e90e..cfabc39cf9c 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -15291,6 +15291,11 @@ vue-multiselect@^2.1.0: resolved "https://registry.yarnpkg.com/vue-multiselect/-/vue-multiselect-2.1.6.tgz#5be5d811a224804a15c43a4edbb7485028a89c7f" integrity sha512-s7jmZPlm9FeueJg1RwJtnE9KNPtME/7C8uRWSfp9/yEN4M8XcS/d+bddoyVwVnvFyRh9msFo0HWeW0vTL8Qv+w== +vue-observe-visibility@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/vue-observe-visibility/-/vue-observe-visibility-1.0.0.tgz#17cf1b2caf74022f0f3c95371468ddf2b9573152" + integrity sha512-s5TFh3s3h3Mhd3jaz3zGzkVHKHnc/0C/gNr30olO99+yw2hl3WBhK3ng3/f9OF+qkW4+l7GkmwfAzDAcY3lCFg== + vue-router@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.3.1.tgz#7d0545dd8ebf4f45494ca185b7eccb38d67689c1" diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py index 411679e1cf6..985089ca7f2 100644 --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -192,6 +192,7 @@ def app_pair(global_conf, load_app_kwds=None, wsgi_preflight=True, **kwargs): webapp.add_client_route('/custom_builds') webapp.add_client_route('/interactivetool_entry_points/list') webapp.add_client_route('/library/folders/{folder_id}') + webapp.add_client_route('/library/folders/permissions/{folder_id}') # ==== Done # Indicate that all configuration settings have been provided