diff --git a/client/src/components/Grid/configs/adminUsers.ts b/client/src/components/Grid/configs/adminUsers.ts index 6f373700dd0..994c2d3557a 100644 --- a/client/src/components/Grid/configs/adminUsers.ts +++ b/client/src/components/Grid/configs/adminUsers.ts @@ -1,7 +1,8 @@ -import Filtering from "@/utils/filtering"; -import { withPrefix } from "@/utils/redirect"; import axios from "axios"; +import Filtering, { contains, equals, toBool, type ValidFilter } from "@/utils/filtering"; +import { withPrefix } from "@/utils/redirect"; + /** * Local types */ @@ -11,7 +12,15 @@ type UserEntry = Record; * Request and return data from server */ async function getData(offset: number, limit: number, search: string, sort_by: string, sort_desc: boolean) { - const { data } = await axios.get(withPrefix(`/admin/users_list?sort_by=${sort_by}&sort_desc=${sort_desc}`)); + 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/users_list?${queryString}`)); return [data.rows, data.total_row_count]; } @@ -75,12 +84,31 @@ const fields = [ }, ]; +const validFilters: Record> = { + email: { placeholder: "email", type: String, handler: contains("email"), menuItem: true }, + username: { placeholder: "username", type: String, handler: contains("username"), menuItem: true }, + deleted: { + placeholder: "Filter on deleted visualizations", + type: Boolean, + boolType: "is", + handler: equals("deleted", "deleted", toBool), + menuItem: true, + }, + purged: { + placeholder: "Filter on purged visualizations", + type: Boolean, + boolType: "is", + handler: equals("purged", "purged", toBool), + menuItem: true, + }, +}; + /** * Grid configuration */ export default { fields: fields, - filtering: new Filtering({}, undefined, false, false), + filtering: new Filtering(validFilters, undefined, false, false), getData: getData, plural: "Users", sortBy: "email", diff --git a/client/src/entry/analysis/routes/admin-routes.js b/client/src/entry/analysis/routes/admin-routes.js index 57b4c388867..c4f1f2a2964 100644 --- a/client/src/entry/analysis/routes/admin-routes.js +++ b/client/src/entry/analysis/routes/admin-routes.js @@ -18,12 +18,12 @@ 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 adminUsersGrid from "components/Grid/configs/adminUsers"; import Grid from "components/Grid/Grid"; import GridList from "components/Grid/GridList"; import RegisterForm from "components/Login/RegisterForm"; import Toolshed from "components/Toolshed/Index"; import Admin from "entry/analysis/modules/Admin"; -import adminUsersGrid from "components/Grid/configs/adminUsers"; export default [ { diff --git a/lib/galaxy/web/framework/helpers/grids.py b/lib/galaxy/web/framework/helpers/grids.py index dd5611f7e5f..32bb75746d5 100644 --- a/lib/galaxy/web/framework/helpers/grids.py +++ b/lib/galaxy/web/framework/helpers/grids.py @@ -1051,11 +1051,12 @@ class Grid: class GridData: """ - Specifies the content and format of a grid (data table). + Specifies the content a grid (data table). """ model_class: Optional[type] = None columns: List[GridColumn] = [] + default_limit: int = 1000 def __init__(self): # If a column does not have a model class, set the column's model class @@ -1065,7 +1066,7 @@ class GridData: column.model_class = self.model_class def __call__(self, trans, **kwargs): - limit = kwargs.get("limit", 1000) + limit = kwargs.get("limit", self.default_limit) offset = kwargs.get("offset", 0) # Build initial query diff --git a/lib/galaxy/webapps/galaxy/controllers/admin.py b/lib/galaxy/webapps/galaxy/controllers/admin.py index 83cbd8d2135..ddc554fa192 100644 --- a/lib/galaxy/webapps/galaxy/controllers/admin.py +++ b/lib/galaxy/webapps/galaxy/controllers/admin.py @@ -4,6 +4,7 @@ from typing import Set from sqlalchemy import ( false, func, + true, ) from typing_extensions import TypedDict @@ -19,6 +20,10 @@ from galaxy.exceptions import ( from galaxy.managers.quotas import QuotaManager from galaxy.model import tool_shed_install as install_model from galaxy.model.base import transaction +from galaxy.model.index_filter_util import ( + raw_text_column_filter, + text_column_filter, +) from galaxy.security.validate_user_input import validate_password from galaxy.structured_app import StructuredApp from galaxy.util import ( @@ -26,6 +31,11 @@ from galaxy.util import ( pretty_print_time_interval, sanitize_text, ) +from galaxy.util.search import ( + FilteredTerm, + parse_filters_structured, + RawTextTerm, +) from galaxy.web import url_for from galaxy.web.framework.helpers import ( grids, @@ -142,8 +152,39 @@ class UserListGrid(grids.GridData): ExternalColumn("External", key="external"), ] - def get_current_item(self, trans, **kwargs): - return trans.user + def apply_query_filter(self, trans, query, **kwargs): + INDEX_SEARCH_FILTERS = { + "email": "email", + "username": "username", + "is": "is", + } + search_query = kwargs.get("search") + if search_query: + parsed_search = parse_filters_structured(search_query, INDEX_SEARCH_FILTERS) + for term in parsed_search.terms: + if isinstance(term, FilteredTerm): + key = term.filter + q = term.text + if key == "email": + query = query.filter(text_column_filter(self.model_class.email, term)) + elif key == "username": + query = query.filter(text_column_filter(self.model_class.username, term)) + elif key == "is": + if q == "deleted": + query = query.filter(self.model_class.deleted == true()) + elif q == "purged": + query = query.filter(self.model_class.purged == true()) + elif isinstance(term, RawTextTerm): + query = query.filter( + raw_text_column_filter( + [ + self.model_class.email, + self.model_class.username, + ], + term, + ) + ) + return query class RoleListGrid(grids.Grid):