Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vueify Admin Roles Grid #17118

Merged
merged 14 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions client/src/api/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ export async function getAllRoles() {
const { data } = await getRoles({});
return data;
}

export const deleteRole = fetcher.path("/api/roles/{id}").method("delete").create();
export const purgeRole = fetcher.path("/api/roles/{id}/purge").method("post").create();
export const undeleteRole = fetcher.path("/api/roles/{id}/undelete").method("post").create();
88 changes: 88 additions & 0 deletions client/src/api/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1361,6 +1361,16 @@ export interface paths {
"/api/roles/{id}": {
/** Show */
get: operations["show_api_roles__id__get"];
/** Delete */
delete: operations["delete_api_roles__id__delete"];
};
"/api/roles/{id}/purge": {
/** Purge */
post: operations["purge_api_roles__id__purge_post"];
};
"/api/roles/{id}/undelete": {
/** Undelete */
post: operations["undelete_api_roles__id__undelete_post"];
};
"/api/short_term_storage/{storage_request_id}": {
/** Serve the staged download specified by request ID. */
Expand Down Expand Up @@ -17622,6 +17632,84 @@ export interface operations {
};
};
};
delete_api_roles__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": components["schemas"]["RoleModelResponse"];
};
};
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
purge_api_roles__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": components["schemas"]["RoleModelResponse"];
};
};
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
undelete_api_roles__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": components["schemas"]["RoleModelResponse"];
};
};
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
serve_api_short_term_storage__storage_request_id__get: {
/** Serve the staged download specified by request ID. */
parameters: {
Expand Down
187 changes: 187 additions & 0 deletions client/src/components/Grid/configs/adminRoles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { faEdit, faKey, faPlus, faTrash, faTrashRestore } from "@fortawesome/free-solid-svg-icons";
import { useEventBus } from "@vueuse/core";
import axios from "axios";

import { deleteRole, purgeRole, undeleteRole } from "@/api/roles";
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<string>("grid-router-push");

/**
* Local types
*/
type RoleEntry = Record<string, unknown>;

/**
* 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/roles_list?${queryString}`));
return [data.rows, data.rows_total];
}
davelopez marked this conversation as resolved.
Show resolved Hide resolved

/**
* Actions are grid-wide operations
*/
const actions: ActionArray = [
{
title: "Create New Role",
icon: faPlus,
handler: () => {
emit("/admin/form/create_role");
},
},
];

/**
* Declare columns to be displayed
*/
const fields: FieldArray = [
{
key: "name",
title: "Name",
type: "operations",
operations: [
{
title: "Edit Name/Description",
icon: faEdit,
condition: (data: RoleEntry) => !data.deleted,
handler: (data: RoleEntry) => {
emit(`/admin/form/rename_role?id=${data.id}`);
},
},
{
title: "Edit Permissions",
icon: faKey,
condition: (data: RoleEntry) => !data.deleted,
handler: (data: RoleEntry) => {
emit(`/admin/form/manage_users_and_groups_for_role?id=${data.id}`);
},
},
{
title: "Delete",
icon: faTrash,
condition: (data: RoleEntry) => !data.deleted,
handler: async (data: RoleEntry) => {
try {
await deleteRole({ 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: RoleEntry) => !!data.deleted,
handler: async (data: RoleEntry) => {
try {
await purgeRole({ 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: RoleEntry) => !!data.deleted,
handler: async (data: RoleEntry) => {
try {
await undeleteRole({ 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: "description",
title: "Description",
type: "text",
},
{
key: "type",
title: "Type",
type: "text",
},
{
key: "groups",
title: "Groups",
type: "text",
},
{
key: "users",
title: "Users",
type: "text",
},
{
key: "update_time",
title: "Updated",
type: "date",
},
];

const validFilters: Record<string, ValidFilter<string | boolean | undefined>> = {
name: { placeholder: "name", type: String, handler: contains("name"), menuItem: true },
description: { placeholder: "description", type: String, handler: contains("description"), 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: "roles-grid",
actions: actions,
fields: fields,
filtering: new Filtering(validFilters, undefined, false, false),
getData: getData,
plural: "Roles",
sortBy: "name",
sortDesc: true,
sortKeys: ["description", "name", "update_time"],
title: "Roles",
};

export default gridConfig;
5 changes: 3 additions & 2 deletions client/src/entry/analysis/routes/admin-routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import NotificationsManagement from "components/admin/Notifications/Notification
import ResetMetadata from "components/admin/ResetMetadata";
import SanitizeAllow from "components/admin/SanitizeAllow";
import FormGeneric from "components/Form/FormGeneric";
import adminRolesGridConfig from "components/Grid/configs/adminRoles";
import adminUsersGridConfig from "components/Grid/configs/adminUsers";
import Grid from "components/Grid/Grid";
import GridList from "components/Grid/GridList";
Expand Down Expand Up @@ -148,9 +149,9 @@ export default [
},
{
path: "roles",
component: Grid,
component: GridList,
props: {
urlBase: "admin/roles_list",
gridConfig: adminRolesGridConfig,
},
},
{
Expand Down
53 changes: 53 additions & 0 deletions lib/galaxy/managers/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,59 @@ def create_role(self, trans: ProvidesUserContext, role_definition_model: RoleDef
trans.sa_session.commit()
return role

def delete(self, trans: ProvidesUserContext, role: model.Role) -> model.Role:
role.deleted = True
trans.sa_session.add(role)
with transaction(trans.sa_session):
trans.sa_session.commit()
return role

def purge(self, trans: ProvidesUserContext, role: model.Role) -> model.Role:
# This method should only be called for a Role that has previously been deleted.
# Purging a deleted Role deletes all of the following from the database:
# - UserRoleAssociations where role_id == Role.id
# - DefaultUserPermissions where role_id == Role.id
# - DefaultHistoryPermissions where role_id == Role.id
# - GroupRoleAssociations where role_id == Role.id
# - DatasetPermissionss where role_id == Role.id
if not role.deleted:
raise galaxy.exceptions.RequestParameterInvalidException(
f"Role '{role.name}' has not been deleted, so it cannot be purged."
)
# Delete UserRoleAssociations
for ura in role.users:
user = trans.sa_session.query(trans.app.model.User).get(ura.user_id)
# Delete DefaultUserPermissions for associated users
for dup in user.default_permissions:
if role == dup.role:
trans.sa_session.delete(dup)
# Delete DefaultHistoryPermissions for associated users
for history in user.histories:
for dhp in history.default_permissions:
if role == dhp.role:
trans.sa_session.delete(dhp)
trans.sa_session.delete(ura)
# Delete GroupRoleAssociations
for gra in role.groups:
trans.sa_session.delete(gra)
# Delete DatasetPermissionss
for dp in role.dataset_actions:
trans.sa_session.delete(dp)
with transaction(trans.sa_session):
trans.sa_session.commit()
return role

def undelete(self, trans: ProvidesUserContext, role: model.Role) -> model.Role:
if not role.deleted:
raise galaxy.exceptions.RequestParameterInvalidException(
f"Role '{role.name}' has not been deleted, so it cannot be undeleted."
)
role.deleted = False
trans.sa_session.add(role)
with transaction(trans.sa_session):
trans.sa_session.commit()
return role


def get_roles_by_ids(session: Session, role_ids):
stmt = select(Role).where(Role.id.in_(role_ids))
Expand Down
18 changes: 18 additions & 0 deletions lib/galaxy/webapps/galaxy/api/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,21 @@ def create(
) -> RoleModelResponse:
role = self.role_manager.create_role(trans, role_definition_model)
return role_to_model(role)

@router.delete("/api/roles/{id}", require_admin=True)
def delete(self, id: DecodedDatabaseIdField, trans: ProvidesUserContext = DependsOnTrans) -> RoleModelResponse:
role = self.role_manager.get(trans, id)
role = self.role_manager.delete(trans, role)
return role_to_model(role)

@router.post("/api/roles/{id}/purge", require_admin=True)
def purge(self, id: DecodedDatabaseIdField, trans: ProvidesUserContext = DependsOnTrans) -> RoleModelResponse:
role = self.role_manager.get(trans, id)
role = self.role_manager.purge(trans, role)
return role_to_model(role)

@router.post("/api/roles/{id}/undelete", require_admin=True)
def undelete(self, id: DecodedDatabaseIdField, trans: ProvidesUserContext = DependsOnTrans) -> RoleModelResponse:
role = self.role_manager.get(trans, id)
role = self.role_manager.undelete(trans, role)
return role_to_model(role)
Loading
Loading