From 548b68ee2225137b514c37c39d921fac18cac87e Mon Sep 17 00:00:00 2001 From: rakdutta Date: Wed, 5 Nov 2025 15:33:32 +0530 Subject: [PATCH 01/13] prompt pagination Signed-off-by: rakdutta --- mcpgateway/admin.py | 196 ++++++++++++ mcpgateway/static/admin.js | 121 +++++++- mcpgateway/templates/admin.html | 279 ++---------------- mcpgateway/templates/prompts_partial.html | 57 ++++ .../templates/prompts_selector_items.html | 38 +++ 5 files changed, 431 insertions(+), 260 deletions(-) create mode 100644 mcpgateway/templates/prompts_partial.html create mode 100644 mcpgateway/templates/prompts_selector_items.html diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index 54550e9cf..748d90894 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -56,6 +56,7 @@ from mcpgateway.db import get_db, GlobalConfig, ObservabilitySavedQuery, ObservabilitySpan, ObservabilityTrace from mcpgateway.db import Tool as DbTool from mcpgateway.db import utc_now +from mcpgateway.db import Prompt as DbPrompt from mcpgateway.middleware.rbac import get_current_user_with_permissions, require_permission from mcpgateway.schemas import ( A2AAgentCreate, @@ -5147,6 +5148,201 @@ async def admin_search_tools( return {"tools": tools, "count": len(tools)} +@admin_router.get("/prompts/partial", response_class=HTMLResponse) +async def admin_prompts_partial_html( + request: Request, + page: int = Query(1, ge=1), + per_page: int = Query(50, ge=1), + include_inactive: bool = False, + render: Optional[str] = Query(None), + db: Session = Depends(get_db), + user=Depends(get_current_user_with_permissions), +): + """ + Return paginated prompts HTML partials for the admin UI. + + Supports render=selector for infinite-scroll selector items, render=controls + to return only the pagination controls, or default full table partial. + """ + # Normalize per_page within configured bounds + per_page = max(settings.pagination_min_page_size, min(per_page, settings.pagination_max_page_size)) + + user_email = get_user_email(user) + + # Team scoping + team_service = TeamManagementService(db) + user_teams = await team_service.get_user_teams(user_email) + team_ids = [t.id for t in user_teams] + + # Build base query + query = select(DbPrompt) + if not include_inactive: + query = query.where(DbPrompt.is_active.is_(True)) + + # Access conditions: owner, team, public + access_conditions = [DbPrompt.owner_email == user_email] + if team_ids: + access_conditions.append(and_(DbPrompt.team_id.in_(team_ids), DbPrompt.visibility.in_(["team", "public"]))) + access_conditions.append(DbPrompt.visibility == "public") + + query = query.where(or_(*access_conditions)) + + # Count total items + count_query = select(func.count()).select_from(DbPrompt).where(or_(*access_conditions)) + if not include_inactive: + count_query = count_query.where(DbPrompt.is_active.is_(True)) + + total_items = db.scalar(count_query) or 0 + + # Apply pagination ordering and limits + offset = (page - 1) * per_page + query = query.order_by(DbPrompt.name, DbPrompt.id).offset(offset).limit(per_page) + + prompts_db = list(db.scalars(query).all()) + + # Convert to schemas using PromptService + local_prompt_service = PromptService() + prompts_data = [] + for p in prompts_db: + try: + prompt_dict = await local_prompt_service.get_prompt_details(db, p.id, include_inactive=include_inactive) + if prompt_dict: + prompts_data.append(prompt_dict) + except Exception as e: + LOGGER.warning(f"Failed to convert prompt {p.id} to schema: {e}") + continue + + data = jsonable_encoder(prompts_data) + + # Build pagination metadata + pagination = PaginationMeta( + page=page, + per_page=per_page, + total_items=total_items, + total_pages=math.ceil(total_items / per_page) if per_page > 0 else 0, + has_next=page < math.ceil(total_items / per_page) if per_page > 0 else False, + has_prev=page > 1, + ) + + base_url = f"{settings.app_root_path}/admin/prompts/partial" + links = generate_pagination_links( + base_url=base_url, + page=page, + per_page=per_page, + total_pages=pagination.total_pages, + query_params={"include_inactive": "true"} if include_inactive else {}, + ) + + if render == "controls": + return request.app.state.templates.TemplateResponse( + "pagination_controls.html", + { + "request": request, + "pagination": pagination.model_dump(), + "base_url": base_url, + "hx_target": "#prompts-table-body", + "hx_indicator": "#prompts-loading", + "query_params": {"include_inactive": "true"} if include_inactive else {}, + "root_path": request.scope.get("root_path", ""), + }, + ) + + if render == "selector": + return request.app.state.templates.TemplateResponse( + "prompts_selector_items.html", + { + "request": request, + "data": data, + "pagination": pagination.model_dump(), + "root_path": request.scope.get("root_path", ""), + }, + ) + + return request.app.state.templates.TemplateResponse( + "prompts_partial.html", + { + "request": request, + "data": data, + "pagination": pagination.model_dump(), + "links": links.model_dump() if links else None, + "root_path": request.scope.get("root_path", ""), + "include_inactive": include_inactive, + }, + ) + + +@admin_router.get("/prompts/ids", response_class=JSONResponse) +async def admin_get_all_prompt_ids( + include_inactive: bool = False, + db: Session = Depends(get_db), + user=Depends(get_current_user_with_permissions), +): + """Return all prompt IDs accessible to the current user (select-all helper).""" + user_email = get_user_email(user) + team_service = TeamManagementService(db) + user_teams = await team_service.get_user_teams(user_email) + team_ids = [t.id for t in user_teams] + + query = select(DbPrompt.id) + if not include_inactive: + query = query.where(DbPrompt.is_active.is_(True)) + + access_conditions = [DbPrompt.owner_email == user_email, DbPrompt.visibility == "public"] + if team_ids: + access_conditions.append(and_(DbPrompt.team_id.in_(team_ids), DbPrompt.visibility.in_(["team", "public"]))) + + query = query.where(or_(*access_conditions)) + prompt_ids = [row[0] for row in db.execute(query).all()] + return {"prompt_ids": prompt_ids, "count": len(prompt_ids)} + + +@admin_router.get("/prompts/search", response_class=JSONResponse) +async def admin_search_prompts( + q: str = Query("", description="Search query"), + include_inactive: bool = False, + limit: int = Query(100, ge=1, le=1000), + db: Session = Depends(get_db), + user=Depends(get_current_user_with_permissions), +): + """Search prompts by name or description for selector search.""" + user_email = get_user_email(user) + search_query = q.strip().lower() + if not search_query: + return {"prompts": [], "count": 0} + + team_service = TeamManagementService(db) + user_teams = await team_service.get_user_teams(user_email) + team_ids = [t.id for t in user_teams] + + query = select(DbPrompt.id, DbPrompt.name, DbPrompt.description) + if not include_inactive: + query = query.where(DbPrompt.is_active.is_(True)) + + access_conditions = [DbPrompt.owner_email == user_email, DbPrompt.visibility == "public"] + if team_ids: + access_conditions.append(and_(DbPrompt.team_id.in_(team_ids), DbPrompt.visibility.in_(["team", "public"]))) + + query = query.where(or_(*access_conditions)) + + search_conditions = [func.lower(DbPrompt.name).contains(search_query), func.lower(coalesce(DbPrompt.description, "")).contains(search_query)] + query = query.where(or_(*search_conditions)) + + query = query.order_by( + case( + (func.lower(DbPrompt.name).startswith(search_query), 1), + else_=2, + ), + func.lower(DbPrompt.name), + ).limit(limit) + + results = db.execute(query).all() + prompts = [] + for row in results: + prompts.append({"id": row.id, "name": row.name, "description": row.description}) + + return {"prompts": prompts, "count": len(prompts)} + + @admin_router.get("/tools/{tool_id}", response_model=ToolRead) async def admin_get_tool(tool_id: str, db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions)) -> Dict[str, Any]: """ diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index fc33990d5..edc66295d 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -10565,17 +10565,18 @@ function setupSelectorSearch() { }); } - // Prompts search + // Prompts search (server-side) const searchPrompts = safeGetElement("searchPrompts", true); if (searchPrompts) { + let promptSearchTimeout; searchPrompts.addEventListener("input", function () { - filterSelectorItems( - this.value, - "#associatedPrompts", - ".prompt-item", - "noPromptsMessage", - "searchPromptsQuery", - ); + const searchTerm = this.value; + if (promptSearchTimeout) { + clearTimeout(promptSearchTimeout); + } + promptSearchTimeout = setTimeout(() => { + serverSidePromptSearch(searchTerm); + }, 300); }); } } @@ -17548,6 +17549,110 @@ function updateToolMapping(container) { }); } +/** + * Perform server-side search for prompts and update the prompt list + */ +async function serverSidePromptSearch(searchTerm) { + const container = document.getElementById("associatedPrompts"); + const noResultsMessage = safeGetElement("noPromptsMessage", true); + const searchQuerySpan = safeGetElement("searchPromptsQuery", true); + + if (!container) { + console.error("associatedPrompts container not found"); + return; + } + + // Show loading state + container.innerHTML = ` +
+ + + + +

Searching prompts...

+
+ `; + + if (searchTerm.trim() === "") { + // If search term is empty, reload the default prompt selector + try { + const response = await fetch(`${window.ROOT_PATH}/admin/prompts/partial?page=1&per_page=50&render=selector`); + if (response.ok) { + const html = await response.text(); + container.innerHTML = html; + + // Hide no results message + if (noResultsMessage) { + noResultsMessage.style.display = "none"; + } + + // Initialize prompt mapping if needed + initPromptSelect('associatedPrompts', 'selectedPromptsPills', 'selectedPromptsWarning', 6, 'selectAllPromptsBtn', 'clearAllPromptsBtn'); + } else { + container.innerHTML = '
Failed to load prompts
'; + } + } catch (error) { + console.error("Error loading prompts:", error); + container.innerHTML = '
Error loading prompts
'; + } + return; + } + + try { + const response = await fetch(`${window.ROOT_PATH}/admin/prompts/search?q=${encodeURIComponent(searchTerm)}&limit=100`); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + + if (data.prompts && data.prompts.length > 0) { + let searchResultsHtml = ""; + data.prompts.forEach((prompt) => { + const displayName = prompt.name || prompt.id; + searchResultsHtml += ` + + `; + }); + + container.innerHTML = searchResultsHtml; + + // Initialize prompt select mapping + initPromptSelect('associatedPrompts', 'selectedPromptsPills', 'selectedPromptsWarning', 6, 'selectAllPromptsBtn', 'clearAllPromptsBtn'); + + if (noResultsMessage) { + noResultsMessage.style.display = "none"; + } + } else { + container.innerHTML = ""; + if (noResultsMessage) { + if (searchQuerySpan) { + searchQuerySpan.textContent = searchTerm; + } + noResultsMessage.style.display = "block"; + } + } + } catch (error) { + console.error("Error searching prompts:", error); + container.innerHTML = '
Error searching prompts
'; + if (noResultsMessage) { + noResultsMessage.style.display = "none"; + } + } +} + // Add CSS for streaming indicator animation const style = document.createElement("style"); style.textContent = ` diff --git a/mcpgateway/templates/admin.html b/mcpgateway/templates/admin.html index d4e8b247f..5f51d51cb 100644 --- a/mcpgateway/templates/admin.html +++ b/mcpgateway/templates/admin.html @@ -360,6 +360,8 @@ + + @@ -2191,30 +2193,19 @@

- {% for prompt in prompts %} - - {% endfor %} - + +
+ + + + + Loading prompts... +

-
-
- - - - - - - - - - - - - - - - - {% for prompt in prompts %} - - - - - - - - - - - - - {% endfor %} - -
- ID - - Name - - Description - - Arguments - - Tags - - Owner - - Team - - Visibility - - Status - - Actions -
- {{ prompt.id }} - - {{ prompt.name }} - - {{ prompt.description }} - - {% for arg in prompt.arguments %}{{ arg.name }}{% if not - loop.last %}, {% endif %}{% endfor %} - - {% if prompt.tags %} {% for tag in prompt.tags %} - {{ tag }} - {% endfor %} {% else %} - None - {% endif %} - - {% if prompt.ownerEmail %} - {{ prompt.ownerEmail }} - {% else %} - N/A - {% endif %} - - {% if prompt.team %} - - {{ prompt.team.replace(' ', '
')|safe }} -
- {% else %} - N/A - {% endif %} -
- {% if prompt.visibility == 'private' %} - Private - {% elif prompt.visibility == 'team' %} - Team - {% elif prompt.visibility == 'public' %} - Public - {% else %} - N/A - {% endif %} - - - {{ "Active" if prompt.isActive else "Inactive" }} - - -
- - - - - - - - -
- {% if prompt.isActive %} -
- - -
- {% else %} -
- - -
- {% endif %} -
- -
-
-
-
+
+
+ + + + + Loading prompts...
diff --git a/mcpgateway/templates/prompts_partial.html b/mcpgateway/templates/prompts_partial.html new file mode 100644 index 000000000..ff930079e --- /dev/null +++ b/mcpgateway/templates/prompts_partial.html @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + +{% for prompt in data %} + + + + + + + + + + + +{% endfor %} + +
S. No.NameDescriptionTagsOwnerTeamVisibilityStatusActions
{{ (pagination.page - 1) * pagination.per_page + loop.index }}{{ prompt.name }}{% set clean_desc = (prompt.description or "") | replace('\n', ' ') | replace('\r', ' ') %} {% set refactor_desc = clean_desc | striptags | trim | escape %} {% if refactor_desc | length is greaterthan 220 %} {{ refactor_desc[:400] + "..." }} {% else %} {{ refactor_desc }} {% endif %}{% if prompt.tags %} {% for tag in prompt.tags %}{{ tag }}{% endfor %} {% else %}None{% endif %}{{ prompt.ownerEmail }}{% if prompt.teamName %}{{ prompt.teamName }}{% else %}None{% endif %}{% if prompt.visibility == 'public' %}๐ŸŒ Public{% elif prompt.visibility == 'team' %}๐Ÿ‘ฅ Team{% else %}๐Ÿ”’ Private{% endif %}
{% set enabled = prompt.isActive %}{% if enabled %}Active{% else %}Inactive{% endif %}
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + +
+ {% set base_url = root_path + '/admin/prompts/partial' %} + {% set hx_target = '#prompts-table' %} + {% set hx_indicator = '#prompts-loading' %} + {% set query_params = {'include_inactive': include_inactive} %} + {% include 'pagination_controls.html' %} +
diff --git a/mcpgateway/templates/prompts_selector_items.html b/mcpgateway/templates/prompts_selector_items.html new file mode 100644 index 000000000..bfdbfe395 --- /dev/null +++ b/mcpgateway/templates/prompts_selector_items.html @@ -0,0 +1,38 @@ + + +{% for prompt in data %} + +{% endfor %} + +{% if pagination.has_next %} + +
+ + + + + + Loading more prompts... + +
+ +{% endif %} From 3b4bc1ac0e93c9a8f9fb7429f1fa723017f0db0b Mon Sep 17 00:00:00 2001 From: rakdutta Date: Wed, 5 Nov 2025 15:58:24 +0530 Subject: [PATCH 02/13] prompts pagination Signed-off-by: rakdutta --- mcpgateway/templates/admin.html | 57 ++++++++++++++++++++++- mcpgateway/templates/prompts_partial.html | 10 ++-- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/mcpgateway/templates/admin.html b/mcpgateway/templates/admin.html index 5f51d51cb..a7792cede 100644 --- a/mcpgateway/templates/admin.html +++ b/mcpgateway/templates/admin.html @@ -3692,9 +3692,10 @@

MCP Prompts

@@ -3705,6 +3706,58 @@

MCP Prompts

+ +
+
+ + + + + Loading prompts... +
+
+ + + {# ---- compute safe pagination defaults ---- #} + {% set page = (pagination.page if pagination is defined else (page if page is defined else 1)) %} + {% set per_page = (pagination.per_page if pagination is defined else (per_page if per_page is defined else 50)) %} + {% set total_items = (pagination.total_items if pagination is defined else (total_items if total_items is defined else (prompts|length if prompts is defined else 0))) %} + {% set total_pages = (pagination.total_pages if pagination is defined else ((total_items + per_page - 1) // per_page)) %} + {% set has_prev = page > 1 %} + {% set has_next = (page * per_page) < total_items %} + + {% set pagination = { + 'page': page, + 'per_page': per_page, + 'total_items': total_items, + 'total_pages': total_pages, + 'has_prev': has_prev, + 'has_next': has_next + } %} + + {# build the refresh URL for controls #} + {% set controls_url = root_path + '/admin/prompts/partial' + + '?render=controls' + + '&page=' + page|string + + '&per_page=' + per_page|string + + '&include_inactive=' + (include_inactive|default(false)|string|lower) %} + +
+ + {# helpers used by pagination_controls.html #} + {% set base_url = root_path + '/admin/prompts/partial' %} + {% set hx_target = '#prompts-table' %} + {% set hx_indicator = '#prompts-loading' %} + {% set query_params = {'include_inactive': include_inactive} %} + + {% include 'pagination_controls.html' %} +
+

Add New Prompt diff --git a/mcpgateway/templates/prompts_partial.html b/mcpgateway/templates/prompts_partial.html index ff930079e..bccedeeff 100644 --- a/mcpgateway/templates/prompts_partial.html +++ b/mcpgateway/templates/prompts_partial.html @@ -23,18 +23,18 @@ {{ prompt.name }} {% set clean_desc = (prompt.description or "") | replace('\n', ' ') | replace('\r', ' ') %} {% set refactor_desc = clean_desc | striptags | trim | escape %} {% if refactor_desc | length is greaterthan 220 %} {{ refactor_desc[:400] + "..." }} {% else %} {{ refactor_desc }} {% endif %} {% if prompt.tags %} {% for tag in prompt.tags %}{{ tag }}{% endfor %} {% else %}None{% endif %} - {{ prompt.ownerEmail }} - {% if prompt.teamName %}{{ prompt.teamName }}{% else %}None{% endif %} + {{ prompt.owner_email }} + {% if prompt.team %}{{ prompt.team }}{% else %}None{% endif %} {% if prompt.visibility == 'public' %}๐ŸŒ Public{% elif prompt.visibility == 'team' %}๐Ÿ‘ฅ Team{% else %}๐Ÿ”’ Private{% endif %} -
{% set enabled = prompt.isActive %}{% if enabled %}Active{% else %}Inactive{% endif %}
+
{% set enabled = prompt.is_active %}{% if enabled %}Active{% else %}Inactive{% endif %}
- - + +
From a780c70db8d263c70abe5d39f44ad6a44ed39d1b Mon Sep 17 00:00:00 2001 From: rakdutta Date: Wed, 5 Nov 2025 17:27:50 +0530 Subject: [PATCH 03/13] resources Signed-off-by: rakdutta --- mcpgateway/admin.py | 125 +++++++++ mcpgateway/static/admin.js | 86 ++++++- mcpgateway/templates/admin.html | 242 ++---------------- mcpgateway/templates/resources_partial.html | 65 +++++ .../templates/resources_selector_items.html | 37 +++ 5 files changed, 322 insertions(+), 233 deletions(-) create mode 100644 mcpgateway/templates/resources_partial.html create mode 100644 mcpgateway/templates/resources_selector_items.html diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index 748d90894..bf0bbc35c 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -57,6 +57,7 @@ from mcpgateway.db import Tool as DbTool from mcpgateway.db import utc_now from mcpgateway.db import Prompt as DbPrompt +from mcpgateway.db import Resource as DbResource from mcpgateway.middleware.rbac import get_current_user_with_permissions, require_permission from mcpgateway.schemas import ( A2AAgentCreate, @@ -5271,6 +5272,130 @@ async def admin_prompts_partial_html( ) +@admin_router.get("/resources/partial", response_class=HTMLResponse) +async def admin_resources_partial_html( + request: Request, + page: int = Query(1, ge=1, description="Page number (1-indexed)"), + per_page: int = Query(50, ge=1, le=500, description="Items per page"), + include_inactive: bool = False, + render: Optional[str] = Query(None, description="Render mode: 'controls' for pagination controls only"), + db: Session = Depends(get_db), + user=Depends(get_current_user_with_permissions), +): + """ + Return HTML partial for paginated resources list (HTMX endpoint). + + Mirrors the tools/prompts partial endpoints and supports render=controls. + """ + LOGGER.debug(f"User {get_user_email(user)} requested resources HTML partial (page={page}, per_page={per_page}, render={render})") + + # Normalize per_page + per_page = max(settings.pagination_min_page_size, min(per_page, settings.pagination_max_page_size)) + + user_email = get_user_email(user) + + # Team scoping + team_service = TeamManagementService(db) + user_teams = await team_service.get_user_teams(user_email) + team_ids = [t.id for t in user_teams] + + # Build base query + query = select(DbResource) + + # Apply active/inactive filter + if not include_inactive: + query = query.where(DbResource.is_active.is_(True)) + + # Access conditions: owner, team, public + access_conditions = [DbResource.owner_email == user_email] + if team_ids: + access_conditions.append(and_(DbResource.team_id.in_(team_ids), DbResource.visibility.in_(["team", "public"]))) + access_conditions.append(DbResource.visibility == "public") + + query = query.where(or_(*access_conditions)) + + # Count total items + count_query = select(func.count()).select_from(DbResource).where(or_(*access_conditions)) + if not include_inactive: + count_query = count_query.where(DbResource.is_active.is_(True)) + + total_items = db.scalar(count_query) or 0 + + # Apply pagination ordering and limits + offset = (page - 1) * per_page + query = query.order_by(DbResource.name, DbResource.id).offset(offset).limit(per_page) + + resources_db = list(db.scalars(query).all()) + + # Convert to schemas using ResourceService + local_resource_service = ResourceService() + resources_data = [] + for r in resources_db: + try: + resources_data.append(local_resource_service._convert_resource_to_read(r)) + except Exception as e: + LOGGER.warning(f"Failed to convert resource {getattr(r, 'id', '')} to schema: {e}") + continue + + data = jsonable_encoder(resources_data) + + # Build pagination metadata + pagination = PaginationMeta( + page=page, + per_page=per_page, + total_items=total_items, + total_pages=math.ceil(total_items / per_page) if per_page > 0 else 0, + has_next=page < math.ceil(total_items / per_page) if per_page > 0 else False, + has_prev=page > 1, + ) + + base_url = f"{settings.app_root_path}/admin/resources/partial" + links = generate_pagination_links( + base_url=base_url, + page=page, + per_page=per_page, + total_pages=pagination.total_pages, + query_params={"include_inactive": "true"} if include_inactive else {}, + ) + + if render == "controls": + return request.app.state.templates.TemplateResponse( + "pagination_controls.html", + { + "request": request, + "pagination": pagination.model_dump(), + "base_url": base_url, + "hx_target": "#resources-table-body", + "hx_indicator": "#resources-loading", + "query_params": {"include_inactive": "true"} if include_inactive else {}, + "root_path": request.scope.get("root_path", ""), + }, + ) + + if render == "selector": + return request.app.state.templates.TemplateResponse( + "resources_selector_items.html", + { + "request": request, + "data": data, + "pagination": pagination.model_dump(), + "root_path": request.scope.get("root_path", ""), + }, + ) + + return request.app.state.templates.TemplateResponse( + "resources_partial.html", + { + "request": request, + "data": data, + "pagination": pagination.model_dump(), + "links": links.model_dump() if links else None, + "root_path": request.scope.get("root_path", ""), + "include_inactive": include_inactive, + }, + ) + + @admin_router.get("/prompts/ids", response_class=JSONResponse) async def admin_get_all_prompt_ids( include_inactive: bool = False, diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index edc66295d..711d4548f 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -6816,13 +6816,87 @@ function toggleInactiveItems(type) { return; } - const url = new URL(window.location); - if (checkbox.checked) { - url.searchParams.set("include_inactive", "true"); - } else { - url.searchParams.delete("include_inactive"); + // Update URL in address bar (no navigation) so state is reflected + try { + const urlObj = new URL(window.location); + if (checkbox.checked) { + urlObj.searchParams.set("include_inactive", "true"); + } else { + urlObj.searchParams.delete("include_inactive"); + } + // Use replaceState to avoid adding history entries for every toggle + window.history.replaceState({}, document.title, urlObj.toString()); + } catch (e) { + // ignore (shouldn't happen) + } + + // Try to find the HTMX container that loads this entity's partial + // Prefer an element with hx-get containing the admin partial endpoint + let selector = `[hx-get*="/admin/${type}/partial"]`; + let container = document.querySelector(selector); + + // Fallback to conventional id naming used in templates + if (!container) { + const fallbackId = type === 'tools' ? 'tools-table' : `${type}-list-container`; + container = document.getElementById(fallbackId); + } + + if (!container) { + // If we couldn't find a container, fallback to full-page reload + const fallbackUrl = new URL(window.location); + if (checkbox.checked) { + fallbackUrl.searchParams.set("include_inactive", "true"); + } else { + fallbackUrl.searchParams.delete("include_inactive"); + } + window.location = fallbackUrl; + return; } - window.location = url; + + // Build request URL based on the hx-get attribute or container id + let base = container.getAttribute('hx-get') || container.getAttribute('data-hx-get') || ''; + let reqUrl; + try { + if (base) { + // base may already include query params; construct URL and set include_inactive/page + reqUrl = new URL(base, window.location.origin); + // reset to page 1 when toggling + reqUrl.searchParams.set('page', '1'); + if (checkbox.checked) reqUrl.searchParams.set('include_inactive', 'true'); + else reqUrl.searchParams.delete('include_inactive'); + } else { + // construct from known pattern + const root = window.ROOT_PATH || ''; + reqUrl = new URL(`${root}/admin/${type}/partial?page=1&per_page=50`, window.location.origin); + if (checkbox.checked) reqUrl.searchParams.set('include_inactive', 'true'); + } + } catch (e) { + // fallback to full reload + const fallbackUrl2 = new URL(window.location); + if (checkbox.checked) fallbackUrl2.searchParams.set('include_inactive', 'true'); + else fallbackUrl2.searchParams.delete('include_inactive'); + window.location = fallbackUrl2; + return; + } + + // Determine indicator selector + const indicator = container.getAttribute('hx-indicator') || `#${type}-loading`; + + // Use HTMX to reload only the container (outerHTML swap) + if (window.htmx && typeof window.htmx.ajax === 'function') { + try { + window.htmx.ajax('GET', reqUrl.toString(), { target: container, swap: 'outerHTML', indicator: indicator }); + return; + } catch (e) { + // fall through to full reload + } + } + + // Last resort: reload page with param + const finalUrl = new URL(window.location); + if (checkbox.checked) finalUrl.searchParams.set('include_inactive', 'true'); + else finalUrl.searchParams.delete('include_inactive'); + window.location = finalUrl; } function handleToggleSubmit(event, type) { diff --git a/mcpgateway/templates/admin.html b/mcpgateway/templates/admin.html index a7792cede..5eb97b190 100644 --- a/mcpgateway/templates/admin.html +++ b/mcpgateway/templates/admin.html @@ -3255,234 +3255,22 @@

MCP Resources

- - - - - - - - - - - - - - - - - - {% for resource in resources %} - - - - - - - - - - - - - - {% endfor %} - -
- ID - - URI - - Name - - Description - - MIME Type - - Tags - - Owner - - Team - - Visibility - - Status - - Actions -
- {{ resource.id }} - - {{ resource.uri }} - - {{ resource.name }} - - {{ resource.description or "N/A" }} - - {{ resource.mimeType or "N/A" }} - - {% if resource.tags %} {% for tag in resource.tags %} - {{ tag }} - {% endfor %} {% else %} - None - {% endif %} - - {% if resource.ownerEmail %} - {{ resource.ownerEmail }} - {% else %} - N/A - {% endif %} - - {% if resource.team %} - - {{ resource.team.replace(' ', '
')|safe }} -
- {% else %} - N/A - {% endif %} -
- {% if resource.visibility == 'private' %} - Private - {% elif resource.visibility == 'team' %} - Team - {% elif resource.visibility == 'public' %} - Public - {% else %} - N/A - {% endif %} - - - {{ "Active" if resource.isActive else "Inactive" }} - - -
- - - - - -
- {% if resource.isActive %} - - - - - {% else %} -
- - -
- {% endif %} -
- -
-
-
-
+ +
+ +
+ +
+ +
diff --git a/mcpgateway/templates/resources_partial.html b/mcpgateway/templates/resources_partial.html new file mode 100644 index 000000000..4150c7d5a --- /dev/null +++ b/mcpgateway/templates/resources_partial.html @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + {% for resource in data %} + + + + + + + + + + + + + + {% endfor %} + +
IDURINameDescriptionMIME TypeTagsOwnerTeamVisibilityStatusActions
{{ resource.id }}{{ resource.uri }}{{ resource.name }}{{ resource.description or 'N/A' }}{{ resource.mimeType or 'N/A' }}{% if resource.tags %}{% for tag in resource.tags %}{{ tag }}{% endfor %}{% else %}None{% endif %}{% if resource.ownerEmail %}{{ resource.ownerEmail }}{% else %}N/A{% endif %}{% if resource.team %}{{ resource.team.replace(' ', '
')|safe }}
{% else %}N/A{% endif %}
{% if resource.visibility == 'private' %}Private{% elif resource.visibility == 'team' %}Team{% elif resource.visibility == 'public' %}Public{% else %}N/A{% endif %}{{ 'Active' if resource.isActive else 'Inactive' }} +
+ + +
+ {% if resource.isActive %} +
+ + +
+ {% else %} +
+ + +
+ {% endif %} +
+ +
+
+
+
+ + +
+ {% set base_url = root_path + '/admin/resources/partial' %} + {% set hx_target = '#resources-table-body' %} + {% set hx_indicator = '#resources-loading' %} + {% set query_params = {'include_inactive': include_inactive} %} + {% include 'pagination_controls.html' %} +
diff --git a/mcpgateway/templates/resources_selector_items.html b/mcpgateway/templates/resources_selector_items.html new file mode 100644 index 000000000..413c1a58f --- /dev/null +++ b/mcpgateway/templates/resources_selector_items.html @@ -0,0 +1,37 @@ + + +{% for resource in data %} + +{% endfor %} + +{% if pagination.has_next %} + +
+ + + + + + Loading more resources... + +
+{% endif %} From 5270dcebdec47206b4364687a90c76488f0d2b14 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Wed, 5 Nov 2025 17:58:01 +0530 Subject: [PATCH 04/13] resources pagination Signed-off-by: rakdutta --- mcpgateway/static/admin.js | 106 ++++++++++++++++++++++++++++++++ mcpgateway/templates/admin.html | 35 ++++------- 2 files changed, 118 insertions(+), 23 deletions(-) diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index 711d4548f..c63a912a5 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -6806,6 +6806,112 @@ function initPromptSelect( } } +function initResourceSelect( + selectId, + pillsId, + warnId, + max = 6, + selectBtnId = null, + clearBtnId = null, +) { + const container = document.getElementById(selectId); + const pillsBox = document.getElementById(pillsId); + const warnBox = document.getElementById(warnId); + const clearBtn = clearBtnId ? document.getElementById(clearBtnId) : null; + const selectBtn = selectBtnId ? document.getElementById(selectBtnId) : null; + + if (!container || !pillsBox || !warnBox) { + console.warn(`Resource select elements not found: ${selectId}, ${pillsId}, ${warnId}`); + return; + } + + const pillClasses = "inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full dark:bg-blue-900 dark:text-blue-200"; + + function update() { + try { + const checkboxes = container.querySelectorAll('input[type="checkbox"]'); + const checked = Array.from(checkboxes).filter((cb) => cb.checked); + + // Select All handling + const selectAllInput = container.querySelector('input[name="selectAllResources"]'); + const allIdsInput = container.querySelector('input[name="allResourceIds"]'); + + let count = checked.length; + if (selectAllInput && selectAllInput.value === "true" && allIdsInput) { + try { + const allIds = JSON.parse(allIdsInput.value); + count = allIds.length; + } catch (e) { + console.error("Error parsing allResourceIds:", e); + } + } + + pillsBox.innerHTML = ""; + const maxPillsToShow = 3; + checked.slice(0, maxPillsToShow).forEach((cb) => { + const span = document.createElement("span"); + span.className = pillClasses; + span.textContent = cb.nextElementSibling?.textContent?.trim() || "Unnamed"; + pillsBox.appendChild(span); + }); + + if (count > maxPillsToShow) { + const span = document.createElement("span"); + span.className = pillClasses + " cursor-pointer"; + span.title = "Click to see all selected resources"; + span.textContent = `+${count - maxPillsToShow} more`; + pillsBox.appendChild(span); + } + + if (count > max) { + warnBox.textContent = `Selected ${count} resources. Selecting more than ${max} resources can degrade performance.`; + } else { + warnBox.textContent = ""; + } + } catch (error) { + console.error("Error updating resource select:", error); + } + } + + // Replace and attach clear/select buttons listeners if provided + if (clearBtn && !clearBtn.dataset.listenerAttached) { + clearBtn.dataset.listenerAttached = "true"; + const newClearBtn = clearBtn.cloneNode(true); + newClearBtn.dataset.listenerAttached = "true"; + clearBtn.parentNode.replaceChild(newClearBtn, clearBtn); + + newClearBtn.addEventListener("click", () => { + const checkboxes = container.querySelectorAll('input[type="checkbox"]'); + checkboxes.forEach((cb) => (cb.checked = false)); + update(); + }); + } + + if (selectBtn && !selectBtn.dataset.listenerAttached) { + selectBtn.dataset.listenerAttached = "true"; + const newSelectBtn = selectBtn.cloneNode(true); + newSelectBtn.dataset.listenerAttached = "true"; + selectBtn.parentNode.replaceChild(newSelectBtn, selectBtn); + + newSelectBtn.addEventListener("click", () => { + const checkboxes = container.querySelectorAll('input[type="checkbox"]'); + checkboxes.forEach((cb) => (cb.checked = true)); + update(); + }); + } + + update(); + + if (!container.dataset.changeListenerAttached) { + container.dataset.changeListenerAttached = "true"; + container.addEventListener("change", (e) => { + if (e.target.type === "checkbox") { + update(); + } + }); + } +} + // =================================================================== // INACTIVE ITEMS HANDLING // =================================================================== diff --git a/mcpgateway/templates/admin.html b/mcpgateway/templates/admin.html index 5eb97b190..25abc32de 100644 --- a/mcpgateway/templates/admin.html +++ b/mcpgateway/templates/admin.html @@ -2122,30 +2122,19 @@

- {% for resource in resources %} - - {% endfor %} - + +
+ + + + + Loading resources... +

-
-
- - - - - Loading prompts... -
-
- - -
-
- - - - - Loading prompts... -
-
+
+
+ +
+ +
+ + + + + Loading prompts... +
+
- - {# ---- compute safe pagination defaults ---- #} - {% set page = (pagination.page if pagination is defined else (page if page is defined else 1)) %} - {% set per_page = (pagination.per_page if pagination is defined else (per_page if per_page is defined else 50)) %} - {% set total_items = (pagination.total_items if pagination is defined else (total_items if total_items is defined else (prompts|length if prompts is defined else 0))) %} - {% set total_pages = (pagination.total_pages if pagination is defined else ((total_items + per_page - 1) // per_page)) %} - {% set has_prev = page > 1 %} - {% set has_next = (page * per_page) < total_items %} - - {% set pagination = { - 'page': page, - 'per_page': per_page, - 'total_items': total_items, - 'total_pages': total_pages, - 'has_prev': has_prev, - 'has_next': has_next - } %} - - {# build the refresh URL for controls #} - {% set controls_url = root_path + '/admin/prompts/partial' - + '?render=controls' - + '&page=' + page|string - + '&per_page=' + per_page|string - + '&include_inactive=' + (include_inactive|default(false)|string|lower) %} - -
+ + - {# helpers used by pagination_controls.html #} - {% set base_url = root_path + '/admin/prompts/partial' %} - {% set hx_target = '#prompts-table' %} - {% set hx_indicator = '#prompts-loading' %} - {% set query_params = {'include_inactive': include_inactive} %} +
- {% include 'pagination_controls.html' %} + +
+ +

Add New Prompt diff --git a/mcpgateway/templates/prompts_partial.html b/mcpgateway/templates/prompts_partial.html index bccedeeff..6c34afc4c 100644 --- a/mcpgateway/templates/prompts_partial.html +++ b/mcpgateway/templates/prompts_partial.html @@ -2,32 +2,29 @@ - - - - - - - - - + + + + + + + + + - + {% for prompt in data %} - - - - - - - - - + + + + + + + +
S. No.NameDescriptionTagsOwnerTeamVisibilityStatusActionsS. No.NameDescriptionTagsOwnerTeamVisibilityStatusActions
{{ (pagination.page - 1) * pagination.per_page + loop.index }}{{ prompt.name }}{% set clean_desc = (prompt.description or "") | replace('\n', ' ') | replace('\r', ' ') %} {% set refactor_desc = clean_desc | striptags | trim | escape %} {% if refactor_desc | length is greaterthan 220 %} {{ refactor_desc[:400] + "..." }} {% else %} {{ refactor_desc }} {% endif %}{% if prompt.tags %} {% for tag in prompt.tags %}{{ tag }}{% endfor %} {% else %}None{% endif %}{{ prompt.owner_email }}{% if prompt.team %}{{ prompt.team }}{% else %}None{% endif %}{% if prompt.visibility == 'public' %}๐ŸŒ Public{% elif prompt.visibility == 'team' %}๐Ÿ‘ฅ Team{% else %}๐Ÿ”’ Private{% endif %}
{% set enabled = prompt.is_active %}{% if enabled %}Active{% else %}Inactive{% endif %}
+ {{ (pagination.page - 1) * pagination.per_page + loop.index }}{{ prompt.name }}{% set clean_desc = (prompt.description or "") | replace('\n', ' ') | replace('\r', ' ') %} {% set refactor_desc = clean_desc | striptags | trim | escape %} {% if refactor_desc | length is greaterthan 220 %} {{ refactor_desc[:400] + "..." }} {% else %} {{ refactor_desc }} {% endif %}{% if prompt.tags %} {% for tag in prompt.tags %}{{ tag }}{% endfor %} {% else %}None{% endif %}{{ prompt.owner_email }}{% if prompt.team %}{{ prompt.team }}{% else %}None{% endif %}{% if prompt.visibility == 'public' %}๐ŸŒ Public{% elif prompt.visibility == 'team' %}๐Ÿ‘ฅ Team{% else %}๐Ÿ”’ Private{% endif %}
{% set enabled = prompt.is_active %}{% if enabled %}Active{% else %}Inactive{% endif %}
@@ -50,7 +47,7 @@
{% set base_url = root_path + '/admin/prompts/partial' %} - {% set hx_target = '#prompts-table' %} + {% set hx_target = '#prompts-table-body' %} {% set hx_indicator = '#prompts-loading' %} {% set query_params = {'include_inactive': include_inactive} %} {% include 'pagination_controls.html' %} From 909a3c453ba8f6dc889bc8a538d0522913ad7a77 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Thu, 6 Nov 2025 10:47:57 +0530 Subject: [PATCH 06/13] initresourceselect Signed-off-by: rakdutta --- mcpgateway/services/prompt_service.py | 1 - mcpgateway/static/admin.js | 122 ++++---------------------- 2 files changed, 15 insertions(+), 108 deletions(-) diff --git a/mcpgateway/services/prompt_service.py b/mcpgateway/services/prompt_service.py index 8cb936b05..456ed2461 100644 --- a/mcpgateway/services/prompt_service.py +++ b/mcpgateway/services/prompt_service.py @@ -1110,7 +1110,6 @@ async def get_prompt_details(self, db: Session, prompt_id: Union[int, str], incl >>> result == prompt_dict True """ - logger.info(f"prompt_id:::{prompt_id}") prompt = db.get(DbPrompt, prompt_id) if not prompt: raise PromptNotFoundError(f"Prompt not found: {prompt_id}") diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index c63a912a5..68b00f4fb 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -6618,7 +6618,21 @@ function initResourceSelect( 'input[type="checkbox"]', ); const checked = Array.from(checkboxes).filter((cb) => cb.checked); - const count = checked.length; + //const count = checked.length; + + // Select All handling + const selectAllInput = container.querySelector('input[name="selectAllResources"]'); + const allIdsInput = container.querySelector('input[name="allResourceIds"]'); + + let count = checked.length; + if (selectAllInput && selectAllInput.value === "true" && allIdsInput) { + try { + const allIds = JSON.parse(allIdsInput.value); + count = allIds.length; + } catch (e) { + console.error("Error parsing allResourceIds:", e); + } + } // Rebuild pills safely - show first 3, then summarize the rest pillsBox.innerHTML = ""; @@ -6806,112 +6820,6 @@ function initPromptSelect( } } -function initResourceSelect( - selectId, - pillsId, - warnId, - max = 6, - selectBtnId = null, - clearBtnId = null, -) { - const container = document.getElementById(selectId); - const pillsBox = document.getElementById(pillsId); - const warnBox = document.getElementById(warnId); - const clearBtn = clearBtnId ? document.getElementById(clearBtnId) : null; - const selectBtn = selectBtnId ? document.getElementById(selectBtnId) : null; - - if (!container || !pillsBox || !warnBox) { - console.warn(`Resource select elements not found: ${selectId}, ${pillsId}, ${warnId}`); - return; - } - - const pillClasses = "inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full dark:bg-blue-900 dark:text-blue-200"; - - function update() { - try { - const checkboxes = container.querySelectorAll('input[type="checkbox"]'); - const checked = Array.from(checkboxes).filter((cb) => cb.checked); - - // Select All handling - const selectAllInput = container.querySelector('input[name="selectAllResources"]'); - const allIdsInput = container.querySelector('input[name="allResourceIds"]'); - - let count = checked.length; - if (selectAllInput && selectAllInput.value === "true" && allIdsInput) { - try { - const allIds = JSON.parse(allIdsInput.value); - count = allIds.length; - } catch (e) { - console.error("Error parsing allResourceIds:", e); - } - } - - pillsBox.innerHTML = ""; - const maxPillsToShow = 3; - checked.slice(0, maxPillsToShow).forEach((cb) => { - const span = document.createElement("span"); - span.className = pillClasses; - span.textContent = cb.nextElementSibling?.textContent?.trim() || "Unnamed"; - pillsBox.appendChild(span); - }); - - if (count > maxPillsToShow) { - const span = document.createElement("span"); - span.className = pillClasses + " cursor-pointer"; - span.title = "Click to see all selected resources"; - span.textContent = `+${count - maxPillsToShow} more`; - pillsBox.appendChild(span); - } - - if (count > max) { - warnBox.textContent = `Selected ${count} resources. Selecting more than ${max} resources can degrade performance.`; - } else { - warnBox.textContent = ""; - } - } catch (error) { - console.error("Error updating resource select:", error); - } - } - - // Replace and attach clear/select buttons listeners if provided - if (clearBtn && !clearBtn.dataset.listenerAttached) { - clearBtn.dataset.listenerAttached = "true"; - const newClearBtn = clearBtn.cloneNode(true); - newClearBtn.dataset.listenerAttached = "true"; - clearBtn.parentNode.replaceChild(newClearBtn, clearBtn); - - newClearBtn.addEventListener("click", () => { - const checkboxes = container.querySelectorAll('input[type="checkbox"]'); - checkboxes.forEach((cb) => (cb.checked = false)); - update(); - }); - } - - if (selectBtn && !selectBtn.dataset.listenerAttached) { - selectBtn.dataset.listenerAttached = "true"; - const newSelectBtn = selectBtn.cloneNode(true); - newSelectBtn.dataset.listenerAttached = "true"; - selectBtn.parentNode.replaceChild(newSelectBtn, selectBtn); - - newSelectBtn.addEventListener("click", () => { - const checkboxes = container.querySelectorAll('input[type="checkbox"]'); - checkboxes.forEach((cb) => (cb.checked = true)); - update(); - }); - } - - update(); - - if (!container.dataset.changeListenerAttached) { - container.dataset.changeListenerAttached = "true"; - container.addEventListener("change", (e) => { - if (e.target.type === "checkbox") { - update(); - } - }); - } -} - // =================================================================== // INACTIVE ITEMS HANDLING // =================================================================== From eb96261afd04aa9349240893a5f35b2514423012 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Thu, 6 Nov 2025 10:53:00 +0530 Subject: [PATCH 07/13] lint error fix Signed-off-by: rakdutta --- mcpgateway/static/admin.js | 108 +++++++++++++++++++++++++++---------- 1 file changed, 80 insertions(+), 28 deletions(-) diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index 68b00f4fb..63444a936 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -6618,14 +6618,22 @@ function initResourceSelect( 'input[type="checkbox"]', ); const checked = Array.from(checkboxes).filter((cb) => cb.checked); - //const count = checked.length; - + // const count = checked.length; + // Select All handling - const selectAllInput = container.querySelector('input[name="selectAllResources"]'); - const allIdsInput = container.querySelector('input[name="allResourceIds"]'); + const selectAllInput = container.querySelector( + 'input[name="selectAllResources"]', + ); + const allIdsInput = container.querySelector( + 'input[name="allResourceIds"]', + ); let count = checked.length; - if (selectAllInput && selectAllInput.value === "true" && allIdsInput) { + if ( + selectAllInput && + selectAllInput.value === "true" && + allIdsInput + ) { try { const allIds = JSON.parse(allIdsInput.value); count = allIds.length; @@ -6846,12 +6854,13 @@ function toggleInactiveItems(type) { // Try to find the HTMX container that loads this entity's partial // Prefer an element with hx-get containing the admin partial endpoint - let selector = `[hx-get*="/admin/${type}/partial"]`; + const selector = `[hx-get*="/admin/${type}/partial"]`; let container = document.querySelector(selector); // Fallback to conventional id naming used in templates if (!container) { - const fallbackId = type === 'tools' ? 'tools-table' : `${type}-list-container`; + const fallbackId = + type === "tools" ? "tools-table" : `${type}-list-container`; container = document.getElementById(fallbackId); } @@ -6868,38 +6877,57 @@ function toggleInactiveItems(type) { } // Build request URL based on the hx-get attribute or container id - let base = container.getAttribute('hx-get') || container.getAttribute('data-hx-get') || ''; + const base = + container.getAttribute("hx-get") || + container.getAttribute("data-hx-get") || + ""; let reqUrl; try { if (base) { // base may already include query params; construct URL and set include_inactive/page reqUrl = new URL(base, window.location.origin); // reset to page 1 when toggling - reqUrl.searchParams.set('page', '1'); - if (checkbox.checked) reqUrl.searchParams.set('include_inactive', 'true'); - else reqUrl.searchParams.delete('include_inactive'); + reqUrl.searchParams.set("page", "1"); + if (checkbox.checked) { + reqUrl.searchParams.set("include_inactive", "true"); + } else { + reqUrl.searchParams.delete("include_inactive"); + } } else { // construct from known pattern - const root = window.ROOT_PATH || ''; - reqUrl = new URL(`${root}/admin/${type}/partial?page=1&per_page=50`, window.location.origin); - if (checkbox.checked) reqUrl.searchParams.set('include_inactive', 'true'); + const root = window.ROOT_PATH || ""; + reqUrl = new URL( + `${root}/admin/${type}/partial?page=1&per_page=50`, + window.location.origin, + ); + if (checkbox.checked) { + reqUrl.searchParams.set("include_inactive", "true"); + } } } catch (e) { // fallback to full reload const fallbackUrl2 = new URL(window.location); - if (checkbox.checked) fallbackUrl2.searchParams.set('include_inactive', 'true'); - else fallbackUrl2.searchParams.delete('include_inactive'); + if (checkbox.checked) { + fallbackUrl2.searchParams.set("include_inactive", "true"); + } else { + fallbackUrl2.searchParams.delete("include_inactive"); + } window.location = fallbackUrl2; return; } // Determine indicator selector - const indicator = container.getAttribute('hx-indicator') || `#${type}-loading`; + const indicator = + container.getAttribute("hx-indicator") || `#${type}-loading`; // Use HTMX to reload only the container (outerHTML swap) - if (window.htmx && typeof window.htmx.ajax === 'function') { + if (window.htmx && typeof window.htmx.ajax === "function") { try { - window.htmx.ajax('GET', reqUrl.toString(), { target: container, swap: 'outerHTML', indicator: indicator }); + window.htmx.ajax("GET", reqUrl.toString(), { + target: container, + swap: "outerHTML", + indicator, + }); return; } catch (e) { // fall through to full reload @@ -6908,8 +6936,11 @@ function toggleInactiveItems(type) { // Last resort: reload page with param const finalUrl = new URL(window.location); - if (checkbox.checked) finalUrl.searchParams.set('include_inactive', 'true'); - else finalUrl.searchParams.delete('include_inactive'); + if (checkbox.checked) { + finalUrl.searchParams.set("include_inactive", "true"); + } else { + finalUrl.searchParams.delete("include_inactive"); + } window.location = finalUrl; } @@ -17664,7 +17695,9 @@ async function serverSidePromptSearch(searchTerm) { if (searchTerm.trim() === "") { // If search term is empty, reload the default prompt selector try { - const response = await fetch(`${window.ROOT_PATH}/admin/prompts/partial?page=1&per_page=50&render=selector`); + const response = await fetch( + `${window.ROOT_PATH}/admin/prompts/partial?page=1&per_page=50&render=selector`, + ); if (response.ok) { const html = await response.text(); container.innerHTML = html; @@ -17675,19 +17708,30 @@ async function serverSidePromptSearch(searchTerm) { } // Initialize prompt mapping if needed - initPromptSelect('associatedPrompts', 'selectedPromptsPills', 'selectedPromptsWarning', 6, 'selectAllPromptsBtn', 'clearAllPromptsBtn'); + initPromptSelect( + "associatedPrompts", + "selectedPromptsPills", + "selectedPromptsWarning", + 6, + "selectAllPromptsBtn", + "clearAllPromptsBtn", + ); } else { - container.innerHTML = '
Failed to load prompts
'; + container.innerHTML = + '
Failed to load prompts
'; } } catch (error) { console.error("Error loading prompts:", error); - container.innerHTML = '
Error loading prompts
'; + container.innerHTML = + '
Error loading prompts
'; } return; } try { - const response = await fetch(`${window.ROOT_PATH}/admin/prompts/search?q=${encodeURIComponent(searchTerm)}&limit=100`); + const response = await fetch( + `${window.ROOT_PATH}/admin/prompts/search?q=${encodeURIComponent(searchTerm)}&limit=100`, + ); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } @@ -17718,7 +17762,14 @@ async function serverSidePromptSearch(searchTerm) { container.innerHTML = searchResultsHtml; // Initialize prompt select mapping - initPromptSelect('associatedPrompts', 'selectedPromptsPills', 'selectedPromptsWarning', 6, 'selectAllPromptsBtn', 'clearAllPromptsBtn'); + initPromptSelect( + "associatedPrompts", + "selectedPromptsPills", + "selectedPromptsWarning", + 6, + "selectAllPromptsBtn", + "clearAllPromptsBtn", + ); if (noResultsMessage) { noResultsMessage.style.display = "none"; @@ -17734,7 +17785,8 @@ async function serverSidePromptSearch(searchTerm) { } } catch (error) { console.error("Error searching prompts:", error); - container.innerHTML = '
Error searching prompts
'; + container.innerHTML = + '
Error searching prompts
'; if (noResultsMessage) { noResultsMessage.style.display = "none"; } From fca9aa8ca6319e75685a654030b11afc8e246e9f Mon Sep 17 00:00:00 2001 From: rakdutta Date: Thu, 6 Nov 2025 11:06:39 +0530 Subject: [PATCH 08/13] docstring Signed-off-by: rakdutta --- mcpgateway/admin.py | 88 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 11 deletions(-) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index bf0bbc35c..20bf1f998 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -54,10 +54,10 @@ from mcpgateway.common.models import LogLevel from mcpgateway.config import settings from mcpgateway.db import get_db, GlobalConfig, ObservabilitySavedQuery, ObservabilitySpan, ObservabilityTrace -from mcpgateway.db import Tool as DbTool -from mcpgateway.db import utc_now from mcpgateway.db import Prompt as DbPrompt from mcpgateway.db import Resource as DbResource +from mcpgateway.db import Tool as DbTool +from mcpgateway.db import utc_now from mcpgateway.middleware.rbac import get_current_user_with_permissions, require_permission from mcpgateway.schemas import ( A2AAgentCreate, @@ -5159,11 +5159,29 @@ async def admin_prompts_partial_html( db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), ): - """ - Return paginated prompts HTML partials for the admin UI. + """Return paginated prompts HTML partials for the admin UI. + + This HTMX endpoint returns only the partial HTML used by the admin UI for + prompts. It supports three render modes: + + - default: full table partial (rows + controls) + - ``render="controls"``: return only pagination controls + - ``render="selector"``: return selector items for infinite scroll - Supports render=selector for infinite-scroll selector items, render=controls - to return only the pagination controls, or default full table partial. + Args: + request (Request): FastAPI request object used by the template engine. + page (int): Page number (1-indexed). + per_page (int): Number of items per page (bounded by settings). + include_inactive (bool): If True, include inactive prompts in results. + render (Optional[str]): Render mode; one of None, "controls", "selector". + db (Session): Database session (dependency-injected). + user: Authenticated user object from dependency injection. + + Returns: + Union[HTMLResponse, TemplateResponse]: A rendered template response + containing either the table partial, pagination controls, or selector + items depending on ``render``. The response contains JSON-serializable + encoded prompt data when templates expect it. """ # Normalize per_page within configured bounds per_page = max(settings.pagination_min_page_size, min(per_page, settings.pagination_max_page_size)) @@ -5282,10 +5300,27 @@ async def admin_resources_partial_html( db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), ): - """ - Return HTML partial for paginated resources list (HTMX endpoint). + """Return HTML partial for paginated resources list (HTMX endpoint). + + This endpoint mirrors the behavior of the tools and prompts partial + endpoints. It returns a template fragment suitable for HTMX-based + pagination/infinite-scroll within the admin UI. + + Args: + request (Request): FastAPI request object used by the template engine. + page (int): Page number (1-indexed). + per_page (int): Number of items per page (bounded by settings). + include_inactive (bool): If True, include inactive resources in results. + render (Optional[str]): Render mode; when set to "controls" returns only + pagination controls. Other supported value: "selector" for selector + items used by infinite scroll selectors. + db (Session): Database session (dependency-injected). + user: Authenticated user object from dependency injection. - Mirrors the tools/prompts partial endpoints and supports render=controls. + Returns: + Union[HTMLResponse, TemplateResponse]: Rendered template response with the + resources partial (rows + controls), pagination controls only, or selector + items depending on the ``render`` parameter. """ LOGGER.debug(f"User {get_user_email(user)} requested resources HTML partial (page={page}, per_page={per_page}, render={render})") @@ -5402,7 +5437,21 @@ async def admin_get_all_prompt_ids( db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), ): - """Return all prompt IDs accessible to the current user (select-all helper).""" + """Return all prompt IDs accessible to the current user (select-all helper). + + This endpoint is used by UI "Select All" helpers to fetch only the IDs + of prompts the requesting user can access (owner, team, or public). + + Args: + include_inactive (bool): When True include prompts that are inactive. + db (Session): Database session (injected dependency). + user: Authenticated user object from dependency injection. + + Returns: + dict: A dictionary containing two keys: + - "prompt_ids": List[str] of accessible prompt IDs. + - "count": int number of IDs returned. + """ user_email = get_user_email(user) team_service = TeamManagementService(db) user_teams = await team_service.get_user_teams(user_email) @@ -5429,7 +5478,24 @@ async def admin_search_prompts( db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), ): - """Search prompts by name or description for selector search.""" + """Search prompts by name or description for selector search. + + Performs a case-insensitive search over prompt names and descriptions + and returns a limited list of matching prompts suitable for selector + UIs (id, name, description). + + Args: + q (str): Search query string. + include_inactive (bool): When True include prompts that are inactive. + limit (int): Maximum number of results to return (bounded by the query parameter). + db (Session): Database session (injected dependency). + user: Authenticated user object from dependency injection. + + Returns: + dict: A dictionary containing: + - "prompts": List[dict] where each dict has keys "id", "name", "description". + - "count": int number of matched prompts returned. + """ user_email = get_user_email(user) search_query = q.strip().lower() if not search_query: From bd2fd6ce0228a3d4712602b40ccd6182a407d3f1 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Thu, 6 Nov 2025 11:33:54 +0530 Subject: [PATCH 09/13] pylint Signed-off-by: rakdutta --- mcpgateway/admin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index 20bf1f998..58a3804f5 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -5207,7 +5207,7 @@ async def admin_prompts_partial_html( query = query.where(or_(*access_conditions)) # Count total items - count_query = select(func.count()).select_from(DbPrompt).where(or_(*access_conditions)) + count_query = select(func.count()).select_from(DbPrompt).where(or_(*access_conditions)) # pylint: disable=not-callable if not include_inactive: count_query = count_query.where(DbPrompt.is_active.is_(True)) @@ -5350,7 +5350,7 @@ async def admin_resources_partial_html( query = query.where(or_(*access_conditions)) # Count total items - count_query = select(func.count()).select_from(DbResource).where(or_(*access_conditions)) + count_query = select(func.count()).select_from(DbResource).where(or_(*access_conditions)) # pylint: disable=not-callable if not include_inactive: count_query = count_query.where(DbResource.is_active.is_(True)) @@ -5367,7 +5367,7 @@ async def admin_resources_partial_html( resources_data = [] for r in resources_db: try: - resources_data.append(local_resource_service._convert_resource_to_read(r)) + resources_data.append(local_resource_service._convert_resource_to_read(r)) # pylint: disable=protected-access except Exception as e: LOGGER.warning(f"Failed to convert resource {getattr(r, 'id', '')} to schema: {e}") continue From d561c164365b58617a7465f249bdb884ebf71de4 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Thu, 6 Nov 2025 12:52:28 +0530 Subject: [PATCH 10/13] fix duplicate table heading Signed-off-by: rakdutta --- mcpgateway/templates/prompts_partial.html | 2 +- mcpgateway/templates/resources_partial.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mcpgateway/templates/prompts_partial.html b/mcpgateway/templates/prompts_partial.html index 6c34afc4c..b03086fef 100644 --- a/mcpgateway/templates/prompts_partial.html +++ b/mcpgateway/templates/prompts_partial.html @@ -47,7 +47,7 @@
{% set base_url = root_path + '/admin/prompts/partial' %} - {% set hx_target = '#prompts-table-body' %} + {% set hx_target = '#prompts-table' %} {% set hx_indicator = '#prompts-loading' %} {% set query_params = {'include_inactive': include_inactive} %} {% include 'pagination_controls.html' %} diff --git a/mcpgateway/templates/resources_partial.html b/mcpgateway/templates/resources_partial.html index 4150c7d5a..5d013f0c1 100644 --- a/mcpgateway/templates/resources_partial.html +++ b/mcpgateway/templates/resources_partial.html @@ -58,7 +58,7 @@
{% set base_url = root_path + '/admin/resources/partial' %} - {% set hx_target = '#resources-table-body' %} + {% set hx_target = '#resources-table' %} {% set hx_indicator = '#resources-loading' %} {% set query_params = {'include_inactive': include_inactive} %} {% include 'pagination_controls.html' %} From c304d9de60090da3f7fa334e5882f9b7514e4fd0 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Thu, 6 Nov 2025 12:59:16 +0530 Subject: [PATCH 11/13] dom Signed-off-by: rakdutta --- mcpgateway/admin.py | 81 ++++++++++++++- mcpgateway/static/admin.js | 205 ++++++++++++++++++++++++++++++++++--- 2 files changed, 270 insertions(+), 16 deletions(-) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index 58a3804f5..c4a94f101 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -1040,14 +1040,36 @@ async def admin_add_server(request: Request, db: Session = Depends(get_db), user except json.JSONDecodeError: LOGGER.warning("Failed to parse allToolIds JSON, falling back to checked tools") + # Handle "Select All" for resources + associated_resources_list = form.getlist("associatedResources") + if form.get("selectAllResources") == "true": + all_resource_ids_json = str(form.get("allResourceIds", "[]")) + try: + all_resource_ids = json.loads(all_resource_ids_json) + associated_resources_list = all_resource_ids + LOGGER.info(f"Select All resources enabled: {len(all_resource_ids)} resources selected") + except json.JSONDecodeError: + LOGGER.warning("Failed to parse allResourceIds JSON, falling back to checked resources") + + # Handle "Select All" for prompts + associated_prompts_list = form.getlist("associatedPrompts") + if form.get("selectAllPrompts") == "true": + all_prompt_ids_json = str(form.get("allPromptIds", "[]")) + try: + all_prompt_ids = json.loads(all_prompt_ids_json) + associated_prompts_list = all_prompt_ids + LOGGER.info(f"Select All prompts enabled: {len(all_prompt_ids)} prompts selected") + except json.JSONDecodeError: + LOGGER.warning("Failed to parse allPromptIds JSON, falling back to checked prompts") + server = ServerCreate( id=form.get("id") or None, name=form.get("name"), description=form.get("description"), icon=form.get("icon"), associated_tools=",".join(str(x) for x in associated_tools_list), - associated_resources=",".join(str(x) for x in form.getlist("associatedResources")), - associated_prompts=",".join(str(x) for x in form.getlist("associatedPrompts")), + associated_resources=",".join(str(x) for x in associated_resources_list), + associated_prompts=",".join(str(x) for x in associated_prompts_list), tags=tags, visibility=visibility, ) @@ -1244,14 +1266,36 @@ async def admin_edit_server( except json.JSONDecodeError: LOGGER.warning("Failed to parse allToolIds JSON, falling back to checked tools") + # Handle "Select All" for resources + associated_resources_list = form.getlist("associatedResources") + if form.get("selectAllResources") == "true": + all_resource_ids_json = str(form.get("allResourceIds", "[]")) + try: + all_resource_ids = json.loads(all_resource_ids_json) + associated_resources_list = all_resource_ids + LOGGER.info(f"Select All resources enabled for edit: {len(all_resource_ids)} resources selected") + except json.JSONDecodeError: + LOGGER.warning("Failed to parse allResourceIds JSON, falling back to checked resources") + + # Handle "Select All" for prompts + associated_prompts_list = form.getlist("associatedPrompts") + if form.get("selectAllPrompts") == "true": + all_prompt_ids_json = str(form.get("allPromptIds", "[]")) + try: + all_prompt_ids = json.loads(all_prompt_ids_json) + associated_prompts_list = all_prompt_ids + LOGGER.info(f"Select All prompts enabled for edit: {len(all_prompt_ids)} prompts selected") + except json.JSONDecodeError: + LOGGER.warning("Failed to parse allPromptIds JSON, falling back to checked prompts") + server = ServerUpdate( id=form.get("id"), name=form.get("name"), description=form.get("description"), icon=form.get("icon"), associated_tools=",".join(str(x) for x in associated_tools_list), - associated_resources=",".join(str(x) for x in form.getlist("associatedResources")), - associated_prompts=",".join(str(x) for x in form.getlist("associatedPrompts")), + associated_resources=",".join(str(x) for x in associated_resources_list), + associated_prompts=",".join(str(x) for x in associated_prompts_list), tags=tags, visibility=visibility, team_id=team_id, @@ -5470,6 +5514,35 @@ async def admin_get_all_prompt_ids( return {"prompt_ids": prompt_ids, "count": len(prompt_ids)} +@admin_router.get("/resources/ids", response_class=JSONResponse) +async def admin_get_all_resource_ids( + include_inactive: bool = False, + db: Session = Depends(get_db), + user=Depends(get_current_user_with_permissions), +): + """Return all resource IDs accessible to the current user (select-all helper). + + This endpoint is used by UI "Select All" helpers to fetch only the IDs + of resources the requesting user can access (owner, team, or public). + """ + user_email = get_user_email(user) + team_service = TeamManagementService(db) + user_teams = await team_service.get_user_teams(user_email) + team_ids = [t.id for t in user_teams] + + query = select(DbResource.id) + if not include_inactive: + query = query.where(DbResource.is_active.is_(True)) + + access_conditions = [DbResource.owner_email == user_email, DbResource.visibility == "public"] + if team_ids: + access_conditions.append(and_(DbResource.team_id.in_(team_ids), DbResource.visibility.in_(["team", "public"]))) + + query = query.where(or_(*access_conditions)) + resource_ids = [row[0] for row in db.execute(query).all()] + return {"resource_ids": resource_ids, "count": len(resource_ids)} + + @admin_router.get("/prompts/search", response_class=JSONResponse) async def admin_search_prompts( q: str = Query("", description="Search query"), diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index 63444a936..669d8495b 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -6687,6 +6687,21 @@ function initResourceSelect( 'input[type="checkbox"]', ); checkboxes.forEach((cb) => (cb.checked = false)); + + // Remove any select-all hidden inputs + const selectAllInput = container.querySelector( + 'input[name="selectAllResources"]', + ); + if (selectAllInput) { + selectAllInput.remove(); + } + const allIdsInput = container.querySelector( + 'input[name="allResourceIds"]', + ); + if (allIdsInput) { + allIdsInput.remove(); + } + update(); }); } @@ -6697,12 +6712,61 @@ function initResourceSelect( newSelectBtn.dataset.listenerAttached = "true"; selectBtn.parentNode.replaceChild(newSelectBtn, selectBtn); - newSelectBtn.addEventListener("click", () => { - const checkboxes = container.querySelectorAll( - 'input[type="checkbox"]', - ); - checkboxes.forEach((cb) => (cb.checked = true)); - update(); + newSelectBtn.addEventListener("click", async () => { + const originalText = newSelectBtn.textContent; + newSelectBtn.disabled = true; + newSelectBtn.textContent = "Selecting all resources..."; + + try { + const resp = await fetch(`${window.ROOT_PATH}/admin/resources/ids`); + if (!resp.ok) { + throw new Error("Failed to fetch resource IDs"); + } + const data = await resp.json(); + const allIds = data.resource_ids || []; + + // Check all currently loaded checkboxes + const loadedCheckboxes = container.querySelectorAll( + 'input[type="checkbox"]', + ); + loadedCheckboxes.forEach((cb) => (cb.checked = true)); + + // Add hidden select-all flag + let selectAllInput = container.querySelector( + 'input[name="selectAllResources"]', + ); + if (!selectAllInput) { + selectAllInput = document.createElement("input"); + selectAllInput.type = "hidden"; + selectAllInput.name = "selectAllResources"; + container.appendChild(selectAllInput); + } + selectAllInput.value = "true"; + + // Store IDs as JSON for backend handling + let allIdsInput = container.querySelector( + 'input[name="allResourceIds"]', + ); + if (!allIdsInput) { + allIdsInput = document.createElement("input"); + allIdsInput.type = "hidden"; + allIdsInput.name = "allResourceIds"; + container.appendChild(allIdsInput); + } + allIdsInput.value = JSON.stringify(allIds); + + update(); + + newSelectBtn.textContent = `โœ“ All ${allIds.length} resources selected`; + setTimeout(() => { + newSelectBtn.textContent = originalText; + }, 2000); + } catch (error) { + console.error("Error selecting all resources:", error); + alert("Failed to select all resources. Please try again."); + } finally { + newSelectBtn.disabled = false; + } }); } @@ -6713,6 +6777,33 @@ function initResourceSelect( container.dataset.changeListenerAttached = "true"; container.addEventListener("change", (e) => { if (e.target.type === "checkbox") { + // If Select All mode is active, update the stored IDs array + const selectAllInput = container.querySelector( + 'input[name="selectAllResources"]', + ); + const allIdsInput = container.querySelector( + 'input[name="allResourceIds"]', + ); + + if ( + selectAllInput && + selectAllInput.value === "true" && + allIdsInput + ) { + try { + let allIds = JSON.parse(allIdsInput.value); + const id = e.target.value; + if (e.target.checked) { + if (!allIds.includes(id)) allIds.push(id); + } else { + allIds = allIds.filter((x) => x !== id); + } + allIdsInput.value = JSON.stringify(allIds); + } catch (err) { + console.error("Error updating allResourceIds:", err); + } + } + update(); } }); @@ -6796,6 +6887,21 @@ function initPromptSelect( 'input[type="checkbox"]', ); checkboxes.forEach((cb) => (cb.checked = false)); + + // Remove any select-all hidden inputs + const selectAllInput = container.querySelector( + 'input[name="selectAllPrompts"]', + ); + if (selectAllInput) { + selectAllInput.remove(); + } + const allIdsInput = container.querySelector( + 'input[name="allPromptIds"]', + ); + if (allIdsInput) { + allIdsInput.remove(); + } + update(); }); } @@ -6805,13 +6911,61 @@ function initPromptSelect( const newSelectBtn = selectBtn.cloneNode(true); newSelectBtn.dataset.listenerAttached = "true"; selectBtn.parentNode.replaceChild(newSelectBtn, selectBtn); + newSelectBtn.addEventListener("click", async () => { + const originalText = newSelectBtn.textContent; + newSelectBtn.disabled = true; + newSelectBtn.textContent = "Selecting all prompts..."; - newSelectBtn.addEventListener("click", () => { - const checkboxes = container.querySelectorAll( - 'input[type="checkbox"]', - ); - checkboxes.forEach((cb) => (cb.checked = true)); - update(); + try { + const resp = await fetch(`${window.ROOT_PATH}/admin/prompts/ids`); + if (!resp.ok) { + throw new Error("Failed to fetch prompt IDs"); + } + const data = await resp.json(); + const allIds = data.prompt_ids || []; + + // Check all currently loaded checkboxes + const loadedCheckboxes = container.querySelectorAll( + 'input[type="checkbox"]', + ); + loadedCheckboxes.forEach((cb) => (cb.checked = true)); + + // Add hidden select-all flag + let selectAllInput = container.querySelector( + 'input[name="selectAllPrompts"]', + ); + if (!selectAllInput) { + selectAllInput = document.createElement("input"); + selectAllInput.type = "hidden"; + selectAllInput.name = "selectAllPrompts"; + container.appendChild(selectAllInput); + } + selectAllInput.value = "true"; + + // Store IDs as JSON for backend handling + let allIdsInput = container.querySelector( + 'input[name="allPromptIds"]', + ); + if (!allIdsInput) { + allIdsInput = document.createElement("input"); + allIdsInput.type = "hidden"; + allIdsInput.name = "allPromptIds"; + container.appendChild(allIdsInput); + } + allIdsInput.value = JSON.stringify(allIds); + + update(); + + newSelectBtn.textContent = `โœ“ All ${allIds.length} prompts selected`; + setTimeout(() => { + newSelectBtn.textContent = originalText; + }, 2000); + } catch (error) { + console.error("Error selecting all prompts:", error); + alert("Failed to select all prompts. Please try again."); + } finally { + newSelectBtn.disabled = false; + } }); } @@ -6822,6 +6976,33 @@ function initPromptSelect( container.dataset.changeListenerAttached = "true"; container.addEventListener("change", (e) => { if (e.target.type === "checkbox") { + // If Select All mode is active, update the stored IDs array + const selectAllInput = container.querySelector( + 'input[name="selectAllPrompts"]', + ); + const allIdsInput = container.querySelector( + 'input[name="allPromptIds"]', + ); + + if ( + selectAllInput && + selectAllInput.value === "true" && + allIdsInput + ) { + try { + let allIds = JSON.parse(allIdsInput.value); + const id = e.target.value; + if (e.target.checked) { + if (!allIds.includes(id)) allIds.push(id); + } else { + allIds = allIds.filter((x) => x !== id); + } + allIdsInput.value = JSON.stringify(allIds); + } catch (err) { + console.error("Error updating allPromptIds:", err); + } + } + update(); } }); From 628cab17a83ceb179d3c89688cade6d955e3dba8 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Thu, 6 Nov 2025 13:11:42 +0530 Subject: [PATCH 12/13] select all Signed-off-by: rakdutta --- mcpgateway/static/admin.js | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index 669d8495b..e2f7942b5 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -6840,7 +6840,28 @@ function initPromptSelect( 'input[type="checkbox"]', ); const checked = Array.from(checkboxes).filter((cb) => cb.checked); - const count = checked.length; + + // Determine count: if Select All mode is active, use the stored allPromptIds + const selectAllInput = container.querySelector( + 'input[name="selectAllPrompts"]', + ); + const allIdsInput = container.querySelector( + 'input[name="allPromptIds"]', + ); + + let count = checked.length; + if ( + selectAllInput && + selectAllInput.value === "true" && + allIdsInput + ) { + try { + const allIds = JSON.parse(allIdsInput.value); + count = allIds.length; + } catch (e) { + console.error("Error parsing allPromptIds:", e); + } + } // Rebuild pills safely - show first 3, then summarize the rest pillsBox.innerHTML = ""; From 9cf3fa5131083cbb3d50cdaa273a4d00855c0983 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Thu, 6 Nov 2025 13:23:49 +0530 Subject: [PATCH 13/13] lint flake8 Signed-off-by: rakdutta --- mcpgateway/admin.py | 10 ++++++++++ mcpgateway/static/admin.js | 16 ++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index c4a94f101..9ef3cddc8 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -5524,6 +5524,16 @@ async def admin_get_all_resource_ids( This endpoint is used by UI "Select All" helpers to fetch only the IDs of resources the requesting user can access (owner, team, or public). + + Args: + include_inactive (bool): Whether to include inactive resources in the results. + db (Session): Database session dependency. + user: Authenticated user object from dependency injection. + + Returns: + dict: A dictionary containing two keys: + - "resource_ids": List[str] of accessible resource IDs. + - "count": int number of IDs returned. """ user_email = get_user_email(user) team_service = TeamManagementService(db) diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index e2f7942b5..fefd09f6e 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -6718,7 +6718,9 @@ function initResourceSelect( newSelectBtn.textContent = "Selecting all resources..."; try { - const resp = await fetch(`${window.ROOT_PATH}/admin/resources/ids`); + const resp = await fetch( + `${window.ROOT_PATH}/admin/resources/ids`, + ); if (!resp.ok) { throw new Error("Failed to fetch resource IDs"); } @@ -6794,7 +6796,9 @@ function initResourceSelect( let allIds = JSON.parse(allIdsInput.value); const id = e.target.value; if (e.target.checked) { - if (!allIds.includes(id)) allIds.push(id); + if (!allIds.includes(id)) { + allIds.push(id); + } } else { allIds = allIds.filter((x) => x !== id); } @@ -6938,7 +6942,9 @@ function initPromptSelect( newSelectBtn.textContent = "Selecting all prompts..."; try { - const resp = await fetch(`${window.ROOT_PATH}/admin/prompts/ids`); + const resp = await fetch( + `${window.ROOT_PATH}/admin/prompts/ids`, + ); if (!resp.ok) { throw new Error("Failed to fetch prompt IDs"); } @@ -7014,7 +7020,9 @@ function initPromptSelect( let allIds = JSON.parse(allIdsInput.value); const id = e.target.value; if (e.target.checked) { - if (!allIds.includes(id)) allIds.push(id); + if (!allIds.includes(id)) { + allIds.push(id); + } } else { allIds = allIds.filter((x) => x !== id); }