From 7b65a63a01f9f32f772ca67ecabae5eb1e8da3f0 Mon Sep 17 00:00:00 2001 From: guerler Date: Sat, 2 Dec 2023 20:22:21 +0300 Subject: [PATCH] Move group operations from legacy admin controller to api endpoint, and link to client --- client/src/api/groups.ts | 6 +- client/src/api/schema/schema.ts | 90 +++++++++ .../components/Grid/configs/adminGroups.ts | 176 ++++++++++++++++++ lib/galaxy/managers/groups.py | 33 ++++ lib/galaxy/webapps/galaxy/api/groups.py | 15 ++ .../webapps/galaxy/controllers/admin.py | 60 ------ 6 files changed, 319 insertions(+), 61 deletions(-) create mode 100644 client/src/components/Grid/configs/adminGroups.ts diff --git a/client/src/api/groups.ts b/client/src/api/groups.ts index 4f0c3e236e87..2365bcc838e0 100644 --- a/client/src/api/groups.ts +++ b/client/src/api/groups.ts @@ -1,9 +1,13 @@ import axios from "axios"; -import { components } from "@/api/schema"; +import { components, fetcher } from "@/api/schema"; type GroupModel = components["schemas"]["GroupModel"]; export async function getAllGroups(): Promise { const { data } = await axios.get("/api/groups"); return data; } + +export const deleteGroup = fetcher.path("/api/groups/{id}").method("delete").create(); +export const purgeGroup = fetcher.path("/api/groups/{id}/purge").method("post").create(); +export const undeleteGroup = fetcher.path("/api/groups/{id}/undelete").method("post").create(); diff --git a/client/src/api/schema/schema.ts b/client/src/api/schema/schema.ts index cb551a3c25e3..18e4fa72eaa3 100644 --- a/client/src/api/schema/schema.ts +++ b/client/src/api/schema/schema.ts @@ -451,6 +451,18 @@ export interface paths { */ delete: operations["delete_api_groups__group_id__users__user_id__delete"]; }; + "/api/groups/{id}": { + /** Delete */ + delete: operations["delete_api_groups__id__delete"]; + }; + "/api/groups/{id}/purge": { + /** Purge */ + post: operations["purge_api_groups__id__purge_post"]; + }; + "/api/groups/{id}/undelete": { + /** Undelete */ + post: operations["undelete_api_groups__id__undelete_post"]; + }; "/api/help/forum/search": { /** * Search the Galaxy Help forum. @@ -12385,6 +12397,84 @@ export interface operations { }; }; }; + delete_api_groups__id__delete: { + /** Delete */ + parameters: { + /** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */ + header?: { + "run-as"?: string; + }; + path: { + id: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": Record; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + purge_api_groups__id__purge_post: { + /** Purge */ + parameters: { + /** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */ + header?: { + "run-as"?: string; + }; + path: { + id: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": Record; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + undelete_api_groups__id__undelete_post: { + /** Undelete */ + parameters: { + /** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */ + header?: { + "run-as"?: string; + }; + path: { + id: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": Record; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; search_forum_api_help_forum_search_get: { /** * Search the Galaxy Help forum. diff --git a/client/src/components/Grid/configs/adminGroups.ts b/client/src/components/Grid/configs/adminGroups.ts new file mode 100644 index 000000000000..1c97fddff426 --- /dev/null +++ b/client/src/components/Grid/configs/adminGroups.ts @@ -0,0 +1,176 @@ +import { faEdit, faKey, faPlus, faTrash, faTrashRestore } from "@fortawesome/free-solid-svg-icons"; +import { useEventBus } from "@vueuse/core"; +import axios from "axios"; + +import { deleteGroup, purgeGroup, undeleteGroup } from "@/api/groups"; +import Filtering, { contains, equals, toBool, type ValidFilter } from "@/utils/filtering"; +import { withPrefix } from "@/utils/redirect"; +import { errorMessageAsString } from "@/utils/simple-error"; + +import type { ActionArray, FieldArray, GridConfig } from "./types"; + +const { emit } = useEventBus("grid-router-push"); + +/** + * Local types + */ +type GroupEntry = Record; + +/** + * Request and return data from server + */ +async function getData(offset: number, limit: number, search: string, sort_by: string, sort_desc: boolean) { + const query = { + limit: String(limit), + offset: String(offset), + search: search, + sort_by: sort_by, + sort_desc: String(sort_desc), + }; + const queryString = new URLSearchParams(query).toString(); + const { data } = await axios.get(withPrefix(`/admin/groups_list?${queryString}`)); + return [data.rows, data.rows_total]; +} + +/** + * Actions are grid-wide operations + */ +const actions: ActionArray = [ + { + title: "Create New Group", + icon: faPlus, + handler: () => { + emit("/admin/form/create_group"); + }, + }, +]; + +/** + * Declare columns to be displayed + */ +const fields: FieldArray = [ + { + key: "name", + title: "Name", + type: "operations", + operations: [ + { + title: "Edit Name/Description", + icon: faEdit, + condition: (data: GroupEntry) => !data.deleted, + handler: (data: GroupEntry) => { + emit(`/admin/form/rename_group?id=${data.id}`); + }, + }, + { + title: "Edit Permissions", + icon: faKey, + condition: (data: GroupEntry) => !data.deleted, + handler: (data: GroupEntry) => { + emit(`/admin/form/manage_users_and_groups_for_role?id=${data.id}`); + }, + }, + { + title: "Delete", + icon: faTrash, + condition: (data: GroupEntry) => !data.deleted, + handler: async (data: GroupEntry) => { + try { + await deleteGroup({ id: String(data.id) }); + return { + status: "success", + message: `'${data.name}' has been deleted.`, + }; + } catch (e) { + return { + status: "danger", + message: `Failed to delete '${data.name}': ${errorMessageAsString(e)}`, + }; + } + }, + }, + { + title: "Purge", + icon: faTrash, + condition: (data: GroupEntry) => !!data.deleted, + handler: async (data: GroupEntry) => { + try { + await purgeGroup({ id: String(data.id) }); + return { + status: "success", + message: `'${data.name}' has been purged.`, + }; + } catch (e) { + return { + status: "danger", + message: `Failed to purge '${data.name}': ${errorMessageAsString(e)}`, + }; + } + }, + }, + { + title: "Restore", + icon: faTrashRestore, + condition: (data: GroupEntry) => !!data.deleted, + handler: async (data: GroupEntry) => { + try { + await undeleteGroup({ id: String(data.id) }); + return { + status: "success", + message: `'${data.name}' has been restored.`, + }; + } catch (e) { + return { + status: "danger", + message: `Failed to restore '${data.name}': ${errorMessageAsString(e)}`, + }; + } + }, + }, + ], + }, + { + key: "roles", + title: "Roles", + type: "text", + }, + { + key: "users", + title: "Users", + type: "text", + }, + { + key: "update_time", + title: "Updated", + type: "date", + }, +]; + +const validFilters: Record> = { + name: { placeholder: "name", type: String, handler: contains("name"), menuItem: true }, + deleted: { + placeholder: "Filter on deleted entries", + type: Boolean, + boolType: "is", + handler: equals("deleted", "deleted", toBool), + menuItem: true, + }, +}; + +/** + * Grid configuration + */ +const gridConfig: GridConfig = { + id: "groups-grid", + actions: actions, + fields: fields, + filtering: new Filtering(validFilters, undefined, false, false), + getData: getData, + plural: "Groups", + sortBy: "name", + sortDesc: true, + sortKeys: ["name", "update_time"], + title: "Groups", +}; + +export default gridConfig; diff --git a/lib/galaxy/managers/groups.py b/lib/galaxy/managers/groups.py index e8919d4db72a..270348e44c86 100644 --- a/lib/galaxy/managers/groups.py +++ b/lib/galaxy/managers/groups.py @@ -101,6 +101,39 @@ def update(self, trans: ProvidesAppContext, group_id: int, payload: GroupCreateP item["url"] = self._url_for(trans, "show_group", group_id=encoded_id) return item + def delete(self, trans: ProvidesAppContext, group_id: int): + group = self._get_group(trans.sa_session, group_id) + group.deleted = True + trans.sa_session.add(group) + with transaction(trans.sa_session): + trans.sa_session.commit() + + def purge(self, trans: ProvidesAppContext, group_id: int): + group = self._get_group(trans.sa_session, group_id) + if not group.deleted: + raise galaxy.exceptions.RequestParameterInvalidException( + f"Group '{groups.name}' has not been deleted, so it cannot be purged." + ) + # Delete UserGroupAssociations + for uga in group.users: + trans.sa_session.delete(uga) + # Delete GroupRoleAssociations + for gra in group.roles: + trans.sa_session.delete(gra) + with transaction(trans.sa_session): + trans.sa_session.commit() + + def undelete(self, trans: ProvidesAppContext, group_id: int): + group = self._get_group(trans.sa_session, group_id) + if not group.deleted: + raise galaxy.exceptions.RequestParameterInvalidException( + f"Group '{groups.name}' has not been deleted, so it cannot be undeleted." + ) + group.deleted = False + trans.sa_session.add(group) + with transaction(trans.sa_session): + trans.sa_session.commit() + def _url_for(self, trans, name, **kwargs): return trans.url_builder(name, **kwargs) diff --git a/lib/galaxy/webapps/galaxy/api/groups.py b/lib/galaxy/webapps/galaxy/api/groups.py index 2b4c7e1ca1d5..9b6b88ff6546 100644 --- a/lib/galaxy/webapps/galaxy/api/groups.py +++ b/lib/galaxy/webapps/galaxy/api/groups.py @@ -82,3 +82,18 @@ def update( payload: GroupCreatePayload = Body(...), ) -> GroupResponse: return self.manager.update(trans, group_id, payload) + + @router.delete("/api/groups/{id}", require_admin=True) + def delete(self, id: DecodedDatabaseIdField, trans: ProvidesAppContext = DependsOnTrans): + group = self.manager.get(trans, id) + self.manager.delete(trans, group) + + @router.post("/api/groups/{id}/purge", require_admin=True) + def purge(self, id: DecodedDatabaseIdField, trans: ProvidesAppContext = DependsOnTrans): + group = self.manager.get(trans, id) + self.manager.purge(trans, group) + + @router.post("/api/groups/{id}/undelete", require_admin=True) + def undelete(self, id: DecodedDatabaseIdField, trans: ProvidesAppContext = DependsOnTrans): + group = self.manager.get(trans, id) + self.manager.undelete(trans, group) diff --git a/lib/galaxy/webapps/galaxy/controllers/admin.py b/lib/galaxy/webapps/galaxy/controllers/admin.py index a6239cf46c03..33d0875563b7 100644 --- a/lib/galaxy/webapps/galaxy/controllers/admin.py +++ b/lib/galaxy/webapps/galaxy/controllers/admin.py @@ -1002,23 +1002,6 @@ def manage_users_and_groups_for_role(self, trans, payload=None, **kwd): @web.legacy_expose_api @web.require_admin def groups_list(self, trans, **kwargs): - message = kwargs.get("message") - status = kwargs.get("status") - if "operation" in kwargs: - id = kwargs.get("id") - if not id: - return self.message_exception(trans, f"Invalid group id ({str(id)}) received.") - ids = util.listify(id) - operation = kwargs["operation"].lower().replace("+", " ") - if operation == "delete": - message, status = self._delete_group(trans, ids) - elif operation == "undelete": - message, status = self._undelete_group(trans, ids) - elif operation == "purge": - message, status = self._purge_group(trans, ids) - if message and status: - kwargs["message"] = util.sanitize_text(message) - kwargs["status"] = status return self.group_list_grid(trans, **kwargs) @web.legacy_expose_api @@ -1201,49 +1184,6 @@ def create_group(self, trans, payload=None, **kwd): ) return {"message": message} - def _delete_group(self, trans, ids): - message = "Deleted %d groups: " % len(ids) - for group_id in ids: - group = get_group(trans, group_id) - group.deleted = True - trans.sa_session.add(group) - with transaction(trans.sa_session): - trans.sa_session.commit() - message += f" {group.name} " - return (message, "done") - - def _undelete_group(self, trans, ids): - count = 0 - undeleted_groups = "" - for group_id in ids: - group = get_group(trans, group_id) - if not group.deleted: - return (f"Group '{group.name}' has not been deleted, so it cannot be undeleted.", "error") - group.deleted = False - trans.sa_session.add(group) - with transaction(trans.sa_session): - trans.sa_session.commit() - count += 1 - undeleted_groups += f" {group.name}" - return ("Undeleted %d groups: %s" % (count, undeleted_groups), "done") - - def _purge_group(self, trans, ids): - message = "Purged %d groups: " % len(ids) - for group_id in ids: - group = get_group(trans, group_id) - if not group.deleted: - return (f"Group '{group.name}' has not been deleted, so it cannot be purged.", "error") - # Delete UserGroupAssociations - for uga in group.users: - trans.sa_session.delete(uga) - # Delete GroupRoleAssociations - for gra in group.roles: - trans.sa_session.delete(gra) - with transaction(trans.sa_session): - trans.sa_session.commit() - message += f" {group.name} " - return (message, "done") - @web.expose @web.require_admin def create_new_user(self, trans, **kwd):