Skip to content

Commit 697b9bb

Browse files
bogdanmariusc10Bogdan-Marius-Catanuscrivetimihai
authored andcommitted
fix(ui): Admin UI "Select All" now respects search filters for tools/resources/prompts (#3968)
* fix(ui): Admin UI Select All now respects search filters for tools/resources/prompts Add search query parameter 'q' to /admin/tools/ids, /admin/resources/ids, and /admin/prompts/ids endpoints. Update frontend selectors to pass active search term to backend APIs. Support both Add Server and Edit Server modes by detecting appropriate search input IDs. Add comprehensive unit tests covering search functionality with various filter combinations. Closes #3950 Signed-off-by: Bogdan-Marius-Catanus <bogdan-marius.catanus@ibm.com> Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> * fix(ui): port Select All search filter to modular JS, fix team-scope and search field bugs - Port search query parameter passing from deleted monolithic admin.js to modular files: tools.js, resources.js, prompts.js in admin_ui/ - Remove duplicate include_inactive filter in prompt and resource IDs endpoints - Fix prompt IDs endpoint search fields to match HTML partial (original_name, display_name instead of name) for consistent search results - Add include_public parameter to all 3 IDs endpoints so Select All respects the "View Public" checkbox state, matching the visible filtered list in team-scoped mode - Pass include_public from frontend Select All handlers by reading the view-public checkbox for both add and edit server modes - Add JS unit tests for Select All search parameter passing in all 3 modules - Add Python tests for include_public parameter Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> --------- Signed-off-by: Bogdan-Marius-Catanus <bogdan-marius.catanus@ibm.com> Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> Co-authored-by: Bogdan-Marius-Catanus <bogdan-marius.catanus@ibm.com> Co-authored-by: Mihai Criveti <crivetimihai@gmail.com>
1 parent 24f138f commit 697b9bb

10 files changed

Lines changed: 913 additions & 44 deletions

File tree

.secrets.baseline

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"files": "package-lock.json|Cargo.lock|^.secrets.baseline$|scripts/sign_image.sh|scripts/zap|sonar-project.properties|^/Users/brian/dev/github.ibm.com/contextforge-org/sps-pipeline-config/.secrets.baseline$|^./.secrets.baseline$",
44
"lines": null
55
},
6-
"generated_at": "2026-04-04T22:57:41Z",
6+
"generated_at": "2026-04-05T09:07:27Z",
77
"plugins_used": [
88
{
99
"name": "AWSKeyDetector"
@@ -5110,7 +5110,7 @@
51105110
"hashed_secret": "85b60d811d16ff56b3654587d4487f713bfa33b7",
51115111
"is_secret": false,
51125112
"is_verified": false,
5113-
"line_number": 14976,
5113+
"line_number": 15018,
51145114
"type": "Secret Keyword",
51155115
"verified_result": null
51165116
}
@@ -10326,31 +10326,31 @@
1032610326
"hashed_secret": "c00dbbc9dadfbe1e232e93a729dd4752fade0abf",
1032710327
"is_secret": false,
1032810328
"is_verified": false,
10329-
"line_number": 14059,
10329+
"line_number": 14062,
1033010330
"type": "Secret Keyword",
1033110331
"verified_result": null
1033210332
},
1033310333
{
1033410334
"hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511",
1033510335
"is_secret": false,
1033610336
"is_verified": false,
10337-
"line_number": 16816,
10337+
"line_number": 16819,
1033810338
"type": "Secret Keyword",
1033910339
"verified_result": null
1034010340
},
1034110341
{
1034210342
"hashed_secret": "a4b48a81cdab1e1a5dd37907d6c85ca1c61ddc7c",
1034310343
"is_secret": false,
1034410344
"is_verified": false,
10345-
"line_number": 16835,
10345+
"line_number": 16838,
1034610346
"type": "Secret Keyword",
1034710347
"verified_result": null
1034810348
},
1034910349
{
1035010350
"hashed_secret": "dc8002865f92070749b264e76045b04fa3b8de71",
1035110351
"is_secret": false,
1035210352
"is_verified": false,
10353-
"line_number": 20390,
10353+
"line_number": 20393,
1035410354
"type": "Secret Keyword",
1035510355
"verified_result": null
1035610356
}

mcpgateway/admin.py

Lines changed: 74 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8776,9 +8776,11 @@ async def admin_tool_ops_partial(
87768776
@admin_router.get("/tools/ids", response_class=JSONResponse)
87778777
@require_permission("tools.read", allow_admin_bypass=False)
87788778
async def admin_get_all_tool_ids(
8779+
q: str = Query("", description="Search query"),
87798780
include_inactive: bool = False,
87808781
gateway_id: Optional[str] = Query(None, description="Filter by gateway ID(s), comma-separated"),
87818782
team_id: Optional[str] = Depends(_validated_team_id_param),
8783+
include_public: bool = False,
87828784
db: Session = Depends(get_db),
87838785
user=Depends(get_current_user_with_permissions),
87848786
):
@@ -8788,9 +8790,11 @@ async def admin_get_all_tool_ids(
87888790
This is used by "Select All" to get all tool IDs without loading full data.
87898791

87908792
Args:
8793+
q (str): Search query to filter tools by name, ID, or description
87918794
include_inactive (bool): Whether to include inactive tools in the results
87928795
gateway_id (Optional[str]): Filter by gateway ID(s), comma-separated. Accepts the literal value 'null' to indicate NULL gateway_id (local tools).
87938796
team_id (Optional[str]): Filter by team ID.
8797+
include_public (bool): Whether to include all platform-public tools when filtering by team.
87948798
db (Session): Database session dependency
87958799
user: Current user making the request
87968800

@@ -8807,6 +8811,21 @@ async def admin_get_all_tool_ids(
88078811
if not include_inactive:
88088812
query = query.where(DbTool.enabled.is_(True))
88098813

8814+
# Apply search filter if provided
8815+
if q:
8816+
search_query = _normalize_search_query(q)
8817+
if search_query:
8818+
search_conditions = [
8819+
_like_contains(func.lower(DbTool.id), search_query),
8820+
_like_contains(func.lower(DbTool.original_name), search_query),
8821+
_like_contains(func.lower(coalesce(DbTool.display_name, "")), search_query),
8822+
_like_contains(func.lower(coalesce(DbTool.custom_name, "")), search_query),
8823+
_like_contains(func.lower(coalesce(DbTool.description, "")), search_query),
8824+
_like_contains(func.lower(coalesce(DbTool.url, "")), search_query),
8825+
]
8826+
query = query.where(or_(*search_conditions))
8827+
LOGGER.debug(f"Filtering tool IDs by search query: {search_query}")
8828+
88108829
# Apply optional gateway/server scoping (comma-separated ids). Accepts the
88118830
# literal value 'null' to indicate NULL gateway_id (local tools).
88128831
if gateway_id:
@@ -8825,22 +8844,19 @@ async def admin_get_all_tool_ids(
88258844
LOGGER.debug(f"Filtering tools by gateway IDs: {non_null_ids}")
88268845

88278846
# Build access conditions
8828-
# When team_id is specified, show items from that team plus all platform-public tools
8829-
# (visibility="public") so the "Select All" count and payload match what is actually
8830-
# visible in the edit UI. Public visibility is platform-wide regardless of team ownership.
8847+
# When team_id is specified, show items from that team; optionally include
8848+
# platform-public items when include_public is set (mirrors the partial endpoint).
88318849
# Otherwise, show all accessible items (All Teams view).
88328850
if team_id:
88338851
if team_id in team_ids:
8834-
# Apply visibility check: team/public resources + user's own resources (including private)
8835-
# Also include all platform-public tools so they can be associated with team-owned
8836-
# virtual servers.
88378852
team_access = [
88388853
and_(DbTool.team_id == team_id, DbTool.visibility.in_(["team", "public"])),
88398854
and_(DbTool.team_id == team_id, DbTool.owner_email == user_email),
8840-
DbTool.visibility == "public",
88418855
]
8856+
if include_public:
8857+
team_access.append(DbTool.visibility == "public")
88428858
query = query.where(or_(*team_access))
8843-
LOGGER.debug(f"Filtering tool IDs by team_id: {team_id}")
8859+
LOGGER.debug(f"Filtering tool IDs by team_id: {team_id}{' (include_public)' if include_public else ''}")
88448860
else:
88458861
LOGGER.warning(f"User {user_email} attempted to filter tool IDs by team {team_id} but is not a member")
88468862
query = query.where(false())
@@ -10020,9 +10036,11 @@ async def admin_resources_partial_html(
1002010036
@admin_router.get("/prompts/ids", response_class=JSONResponse)
1002110037
@require_permission("prompts.read", allow_admin_bypass=False)
1002210038
async def admin_get_all_prompt_ids(
10039+
q: str = Query("", description="Search query"),
1002310040
include_inactive: bool = False,
1002410041
gateway_id: Optional[str] = Query(None, description="Filter by gateway ID(s), comma-separated"),
1002510042
team_id: Optional[str] = Depends(_validated_team_id_param),
10043+
include_public: bool = False,
1002610044
db: Session = Depends(get_db),
1002710045
user=Depends(get_current_user_with_permissions),
1002810046
):
@@ -10032,9 +10050,11 @@ async def admin_get_all_prompt_ids(
1003210050
of prompts the requesting user can access (owner, team, or public).
1003310051

1003410052
Args:
10053+
q (str): Search query to filter prompts by name or description.
1003510054
include_inactive (bool): When True include prompts that are inactive.
1003610055
gateway_id (Optional[str]): Filter by gateway ID(s), comma-separated. Accepts the literal value 'null' to indicate NULL gateway_id (local prompts).
1003710056
team_id (Optional[str]): Filter by team ID.
10057+
include_public (bool): Whether to include all platform-public prompts when filtering by team.
1003810058
db (Session): Database session (injected dependency).
1003910059
user: Authenticated user object from dependency injection.
1004010060

@@ -10048,6 +10068,22 @@ async def admin_get_all_prompt_ids(
1004810068

1004910069
query = select(DbPrompt.id)
1005010070

10071+
if not include_inactive:
10072+
query = query.where(DbPrompt.enabled.is_(True))
10073+
10074+
# Apply search filter if provided
10075+
if q:
10076+
search_query = _normalize_search_query(q)
10077+
if search_query:
10078+
search_conditions = [
10079+
_like_contains(func.lower(DbPrompt.id), search_query),
10080+
_like_contains(func.lower(DbPrompt.original_name), search_query),
10081+
_like_contains(func.lower(coalesce(DbPrompt.display_name, "")), search_query),
10082+
_like_contains(func.lower(coalesce(DbPrompt.description, "")), search_query),
10083+
]
10084+
query = query.where(or_(*search_conditions))
10085+
LOGGER.debug(f"Filtering prompt IDs by search query: {search_query}")
10086+
1005110087
# Apply optional gateway/server scoping
1005210088
if gateway_id:
1005310089
gateway_ids = [gid.strip() for gid in gateway_id.split(",") if gid.strip()]
@@ -10064,27 +10100,20 @@ async def admin_get_all_prompt_ids(
1006410100
query = query.where(DbPrompt.gateway_id.in_(non_null_ids))
1006510101
LOGGER.debug(f"Filtering prompts by gateway IDs: {non_null_ids}")
1006610102

10067-
if not include_inactive:
10068-
query = query.where(DbPrompt.enabled.is_(True))
10069-
1007010103
# Build access conditions
10071-
# When team_id is specified, show items from that team plus all platform-public prompts
10072-
# (visibility="public") so the "Select All" count and payload match what is actually
10073-
# visible in the edit UI. Public visibility is platform-wide regardless of team ownership.
10104+
# When team_id is specified, show items from that team; optionally include
10105+
# platform-public items when include_public is set (mirrors the partial endpoint).
1007410106
# Otherwise, show all accessible items (All Teams view).
1007510107
if team_id:
10076-
# Team-specific view: show prompts from the specified team plus platform-public prompts
1007710108
if team_id in team_ids:
10078-
# Apply visibility check: team/public resources + user's own resources (including private)
10079-
# Also include all platform-public prompts so they can be associated with team-owned
10080-
# virtual servers.
1008110109
team_access = [
1008210110
and_(DbPrompt.team_id == team_id, DbPrompt.visibility.in_(["team", "public"])),
1008310111
and_(DbPrompt.team_id == team_id, DbPrompt.owner_email == user_email),
10084-
DbPrompt.visibility == "public",
1008510112
]
10113+
if include_public:
10114+
team_access.append(DbPrompt.visibility == "public")
1008610115
query = query.where(or_(*team_access))
10087-
LOGGER.debug(f"Filtering prompt IDs by team_id: {team_id}")
10116+
LOGGER.debug(f"Filtering prompt IDs by team_id: {team_id}{' (include_public)' if include_public else ''}")
1008810117
else:
1008910118
# User is not a member of this team, return no results using SQLAlchemy's false()
1009010119
LOGGER.warning(f"User {user_email} attempted to filter prompt IDs by team {team_id} but is not a member")
@@ -10105,9 +10134,11 @@ async def admin_get_all_prompt_ids(
1010510134
@admin_router.get("/resources/ids", response_class=JSONResponse)
1010610135
@require_permission("resources.read", allow_admin_bypass=False)
1010710136
async def admin_get_all_resource_ids(
10137+
q: str = Query("", description="Search query"),
1010810138
include_inactive: bool = False,
1010910139
gateway_id: Optional[str] = Query(None, description="Filter by gateway ID(s), comma-separated"),
1011010140
team_id: Optional[str] = Depends(_validated_team_id_param),
10141+
include_public: bool = False,
1011110142
db: Session = Depends(get_db),
1011210143
user=Depends(get_current_user_with_permissions),
1011310144
):
@@ -10117,9 +10148,11 @@ async def admin_get_all_resource_ids(
1011710148
of resources the requesting user can access (owner, team, or public).
1011810149

1011910150
Args:
10151+
q (str): Search query to filter resources by name, URI, or description.
1012010152
include_inactive (bool): Whether to include inactive resources in the results.
1012110153
gateway_id (Optional[str]): Filter by gateway ID(s), comma-separated. Accepts the literal value 'null' to indicate NULL gateway_id (local resources).
1012210154
team_id (Optional[str]): Filter by team ID.
10155+
include_public (bool): Whether to include all platform-public resources when filtering by team.
1012310156
db (Session): Database session dependency.
1012410157
user: Authenticated user object from dependency injection.
1012510158

@@ -10133,6 +10166,22 @@ async def admin_get_all_resource_ids(
1013310166

1013410167
query = select(DbResource.id)
1013510168

10169+
if not include_inactive:
10170+
query = query.where(DbResource.enabled.is_(True))
10171+
10172+
# Apply search filter if provided
10173+
if q:
10174+
search_query = _normalize_search_query(q)
10175+
if search_query:
10176+
search_conditions = [
10177+
_like_contains(func.lower(DbResource.id), search_query),
10178+
_like_contains(func.lower(DbResource.name), search_query),
10179+
_like_contains(func.lower(coalesce(DbResource.uri, "")), search_query),
10180+
_like_contains(func.lower(coalesce(DbResource.description, "")), search_query),
10181+
]
10182+
query = query.where(or_(*search_conditions))
10183+
LOGGER.debug(f"Filtering resource IDs by search query: {search_query}")
10184+
1013610185
# Apply optional gateway/server scoping
1013710186
if gateway_id:
1013810187
gateway_ids = [gid.strip() for gid in gateway_id.split(",") if gid.strip()]
@@ -10149,27 +10198,20 @@ async def admin_get_all_resource_ids(
1014910198
query = query.where(DbResource.gateway_id.in_(non_null_ids))
1015010199
LOGGER.debug(f"Filtering resources by gateway IDs: {non_null_ids}")
1015110200

10152-
if not include_inactive:
10153-
query = query.where(DbResource.enabled.is_(True))
10154-
1015510201
# Build access conditions
10156-
# When team_id is specified, show items from that team plus all platform-public resources
10157-
# (visibility="public") so the "Select All" count and payload match what is actually
10158-
# visible in the edit UI. Public visibility is platform-wide regardless of team ownership.
10202+
# When team_id is specified, show items from that team; optionally include
10203+
# platform-public items when include_public is set (mirrors the partial endpoint).
1015910204
# Otherwise, show all accessible items (All Teams view).
1016010205
if team_id:
10161-
# Team-specific view: show resources from the specified team plus platform-public resources
1016210206
if team_id in team_ids:
10163-
# Apply visibility check: team/public resources + user's own resources (including private)
10164-
# Also include all platform-public resources so they can be associated with team-owned
10165-
# virtual servers.
1016610207
team_access = [
1016710208
and_(DbResource.team_id == team_id, DbResource.visibility.in_(["team", "public"])),
1016810209
and_(DbResource.team_id == team_id, DbResource.owner_email == user_email),
10169-
DbResource.visibility == "public",
1017010210
]
10211+
if include_public:
10212+
team_access.append(DbResource.visibility == "public")
1017110213
query = query.where(or_(*team_access))
10172-
LOGGER.debug(f"Filtering resource IDs by team_id: {team_id}")
10214+
LOGGER.debug(f"Filtering resource IDs by team_id: {team_id}{' (include_public)' if include_public else ''}")
1017310215
else:
1017410216
# User is not a member of this team, return no results using SQLAlchemy's false()
1017510217
LOGGER.warning(f"User {user_email} attempted to filter resource IDs by team {team_id} but is not a member")

mcpgateway/admin_ui/prompts.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,13 +749,30 @@ export const initPromptSelect = function (
749749
? getSelectedGatewayIds()
750750
: [];
751751
const selectedTeamId = getCurrentTeamId();
752+
const searchInputId =
753+
selectId === "edit-server-prompts"
754+
? "searchEditPrompts"
755+
: "searchPrompts";
756+
const searchInput = document.getElementById(searchInputId);
757+
const searchTerm = searchInput ? searchInput.value.trim() : "";
752758
const params = new URLSearchParams();
753759
if (selectedGatewayIds && selectedGatewayIds.length) {
754760
params.set("gateway_id", selectedGatewayIds.join(","));
755761
}
756762
if (selectedTeamId) {
757763
params.set("team_id", selectedTeamId);
758764
}
765+
if (searchTerm) {
766+
params.set("q", searchTerm);
767+
}
768+
const viewPublicId =
769+
selectId === "edit-server-prompts"
770+
? "edit-server-view-public"
771+
: "add-server-view-public";
772+
const viewPublicCb = document.getElementById(viewPublicId);
773+
if (viewPublicCb && viewPublicCb.checked) {
774+
params.set("include_public", "true");
775+
}
759776
const queryString = params.toString();
760777
const resp = await fetch(
761778
`${window.ROOT_PATH}/admin/prompts/ids${queryString ? `?${queryString}` : ""}`

mcpgateway/admin_ui/resources.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,13 +996,30 @@ export const initResourceSelect = function (
996996
? getSelectedGatewayIds()
997997
: [];
998998
const selectedTeamId = getCurrentTeamId();
999+
const searchInputId =
1000+
selectId === "edit-server-resources"
1001+
? "searchEditResources"
1002+
: "searchResources";
1003+
const searchInput = document.getElementById(searchInputId);
1004+
const searchTerm = searchInput ? searchInput.value.trim() : "";
9991005
const params = new URLSearchParams();
10001006
if (selectedGatewayIds && selectedGatewayIds.length) {
10011007
params.set("gateway_id", selectedGatewayIds.join(","));
10021008
}
10031009
if (selectedTeamId) {
10041010
params.set("team_id", selectedTeamId);
10051011
}
1012+
if (searchTerm) {
1013+
params.set("q", searchTerm);
1014+
}
1015+
const viewPublicId =
1016+
selectId === "edit-server-resources"
1017+
? "edit-server-view-public"
1018+
: "add-server-view-public";
1019+
const viewPublicCb = document.getElementById(viewPublicId);
1020+
if (viewPublicCb && viewPublicCb.checked) {
1021+
params.set("include_public", "true");
1022+
}
10061023
const queryString = params.toString();
10071024
const resp = await fetch(
10081025
`${window.ROOT_PATH}/admin/resources/ids${queryString ? `?${queryString}` : ""}`

mcpgateway/admin_ui/tools.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,13 +1197,30 @@ export const initToolSelect = function (
11971197
? getSelectedGatewayIds()
11981198
: [];
11991199
const selectedTeamId = getCurrentTeamId();
1200+
const searchInputId =
1201+
selectId === "edit-server-tools"
1202+
? "searchEditTools"
1203+
: "searchTools";
1204+
const searchInput = document.getElementById(searchInputId);
1205+
const searchTerm = searchInput ? searchInput.value.trim() : "";
12001206
const params = new URLSearchParams();
12011207
if (selectedGatewayIds && selectedGatewayIds.length) {
12021208
params.set("gateway_id", selectedGatewayIds.join(","));
12031209
}
12041210
if (selectedTeamId) {
12051211
params.set("team_id", selectedTeamId);
12061212
}
1213+
if (searchTerm) {
1214+
params.set("q", searchTerm);
1215+
}
1216+
const viewPublicId =
1217+
selectId === "edit-server-tools"
1218+
? "edit-server-view-public"
1219+
: "add-server-view-public";
1220+
const viewPublicCb = document.getElementById(viewPublicId);
1221+
if (viewPublicCb && viewPublicCb.checked) {
1222+
params.set("include_public", "true");
1223+
}
12071224
const queryString = params.toString();
12081225
const response = await fetch(
12091226
`${window.ROOT_PATH}/admin/tools/ids${queryString ? `?${queryString}` : ""}`

0 commit comments

Comments
 (0)