diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index 292fd7e04..72a75404c 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -5853,6 +5853,7 @@ async def admin_search_tools( q: str = Query("", description="Search query"), include_inactive: bool = False, limit: int = Query(100, ge=1, le=1000, description="Maximum number of results to return"), + gateway_id: Optional[str] = Query(None, description="Filter by gateway ID(s), comma-separated"), db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), ): @@ -5866,6 +5867,7 @@ async def admin_search_tools( q (str): Search query string to match against tool names, IDs, or descriptions include_inactive (bool): Whether to include inactive tools in the search results limit (int): Maximum number of results to return (1-1000) + gateway_id (Optional[str]): Filter by gateway ID(s), comma-separated db (Session): Database session dependency user: Current user making the request @@ -5886,6 +5888,24 @@ async def admin_search_tools( query = select(DbTool.id, DbTool.original_name, DbTool.custom_name, DbTool.display_name, DbTool.description) + # Apply gateway filter if provided. Support special sentinel 'null' to + # request tools with NULL gateway_id (e.g., RestTool/no gateway). + if gateway_id: + gateway_ids = [gid.strip() for gid in gateway_id.split(",") if gid.strip()] + if gateway_ids: + # Treat literal 'null' (case-insensitive) as a request for NULL gateway_id + null_requested = any(gid.lower() == "null" for gid in gateway_ids) + non_null_ids = [gid for gid in gateway_ids if gid.lower() != "null"] + if non_null_ids and null_requested: + query = query.where(or_(DbTool.gateway_id.in_(non_null_ids), DbTool.gateway_id.is_(None))) + LOGGER.debug(f"Filtering tool search by gateway IDs (including NULL): {non_null_ids} + NULL") + elif null_requested: + query = query.where(DbTool.gateway_id.is_(None)) + LOGGER.debug("Filtering tool search by NULL gateway_id (RestTool)") + else: + query = query.where(DbTool.gateway_id.in_(non_null_ids)) + LOGGER.debug(f"Filtering tool search by gateway IDs: {non_null_ids}") + if not include_inactive: query = query.where(DbTool.enabled.is_(True)) @@ -6396,6 +6416,7 @@ async def admin_search_resources( q: str = Query("", description="Search query"), include_inactive: bool = False, limit: int = Query(100, ge=1, le=1000), + gateway_id: Optional[str] = Query(None, description="Filter by gateway ID(s), comma-separated"), db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), ): @@ -6409,6 +6430,7 @@ async def admin_search_resources( q (str): Search query string. include_inactive (bool): When True include resources that are inactive. limit (int): Maximum number of results to return (bounded by the query parameter). + gateway_id (Optional[str]): Filter by gateway ID(s), comma-separated. db (Session): Database session (injected dependency). user: Authenticated user object from dependency injection. @@ -6427,6 +6449,23 @@ async def admin_search_resources( team_ids = [t.id for t in user_teams] query = select(DbResource.id, DbResource.name, DbResource.description) + + # Apply gateway filter if provided + if gateway_id: + gateway_ids = [gid.strip() for gid in gateway_id.split(",") if gid.strip()] + if gateway_ids: + null_requested = any(gid.lower() == "null" for gid in gateway_ids) + non_null_ids = [gid for gid in gateway_ids if gid.lower() != "null"] + if non_null_ids and null_requested: + query = query.where(or_(DbResource.gateway_id.in_(non_null_ids), DbResource.gateway_id.is_(None))) + LOGGER.debug(f"Filtering resource search by gateway IDs (including NULL): {non_null_ids} + NULL") + elif null_requested: + query = query.where(DbResource.gateway_id.is_(None)) + LOGGER.debug("Filtering resource search by NULL gateway_id") + else: + query = query.where(DbResource.gateway_id.in_(non_null_ids)) + LOGGER.debug(f"Filtering resource search by gateway IDs: {non_null_ids}") + if not include_inactive: query = query.where(DbResource.enabled.is_(True)) @@ -6459,6 +6498,7 @@ async def admin_search_prompts( q: str = Query("", description="Search query"), include_inactive: bool = False, limit: int = Query(100, ge=1, le=1000), + gateway_id: Optional[str] = Query(None, description="Filter by gateway ID(s), comma-separated"), db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), ): @@ -6472,6 +6512,7 @@ async def admin_search_prompts( 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). + gateway_id (Optional[str]): Filter by gateway ID(s), comma-separated. db (Session): Database session (injected dependency). user: Authenticated user object from dependency injection. @@ -6490,6 +6531,23 @@ async def admin_search_prompts( team_ids = [t.id for t in user_teams] query = select(DbPrompt.id, DbPrompt.name, DbPrompt.description) + + # Apply gateway filter if provided + if gateway_id: + gateway_ids = [gid.strip() for gid in gateway_id.split(",") if gid.strip()] + if gateway_ids: + null_requested = any(gid.lower() == "null" for gid in gateway_ids) + non_null_ids = [gid for gid in gateway_ids if gid.lower() != "null"] + if non_null_ids and null_requested: + query = query.where(or_(DbPrompt.gateway_id.in_(non_null_ids), DbPrompt.gateway_id.is_(None))) + LOGGER.debug(f"Filtering prompt search by gateway IDs (including NULL): {non_null_ids} + NULL") + elif null_requested: + query = query.where(DbPrompt.gateway_id.is_(None)) + LOGGER.debug("Filtering prompt search by NULL gateway_id") + else: + query = query.where(DbPrompt.gateway_id.in_(non_null_ids)) + LOGGER.debug(f"Filtering prompt search by gateway IDs: {non_null_ids}") + if not include_inactive: query = query.where(DbPrompt.enabled.is_(True)) diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index f3e73d21a..35c00f012 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -5966,7 +5966,7 @@ async function editServer(serverId) { ); resourceCheckboxes.forEach((checkbox) => { - const checkboxValue = parseInt(checkbox.value); + const checkboxValue = checkbox.value; const isChecked = server.associatedResources && server.associatedResources.includes(checkboxValue); @@ -5984,7 +5984,7 @@ async function editServer(serverId) { : document.querySelectorAll('input[name="associatedPrompts"]'); promptCheckboxes.forEach((checkbox) => { - const checkboxValue = parseInt(checkbox.value); + const checkboxValue = checkbox.value; const isChecked = server.associatedPrompts && server.associatedPrompts.includes(checkboxValue); @@ -6085,7 +6085,7 @@ function setEditServerAssociations(server) { : document.querySelectorAll('input[name="associatedResources"]'); resourceCheckboxes.forEach((checkbox) => { - const checkboxValue = parseInt(checkbox.value); + const checkboxValue = checkbox.value; const isChecked = server.associatedResources && server.associatedResources.includes(checkboxValue); @@ -6099,7 +6099,7 @@ function setEditServerAssociations(server) { : document.querySelectorAll('input[name="associatedPrompts"]'); promptCheckboxes.forEach((checkbox) => { - const checkboxValue = parseInt(checkbox.value); + const checkboxValue = checkbox.value; const isChecked = server.associatedPrompts && server.associatedPrompts.includes(checkboxValue); @@ -6285,6 +6285,38 @@ if (window.htmx && !window._toolsHtmxHandlerAttached) { container.dispatchEvent(event); } } + // If we're in the Add Server tools container, restore persisted selections + else if (container.id === "associatedTools") { + try { + const dataAttr = container.getAttribute( + "data-selected-tools", + ); + if (dataAttr) { + const selectedIds = JSON.parse(dataAttr); + if ( + Array.isArray(selectedIds) && + selectedIds.length > 0 + ) { + newCheckboxes.forEach((cb) => { + if (selectedIds.includes(cb.value)) { + cb.checked = true; + } + cb.removeAttribute("data-auto-check"); + }); + + const event = new Event("change", { + bubbles: true, + }); + container.dispatchEvent(event); + } + } + } catch (e) { + console.warn( + "Error restoring associatedTools selections:", + e, + ); + } + } } }, 10); // Small delay to ensure DOM is updated } @@ -6366,7 +6398,7 @@ if (window.htmx && !window._resourcesHtmxHandlerAttached) { try { const associatedResourceIds = JSON.parse(dataAttr); newCheckboxes.forEach((cb) => { - const checkboxValue = parseInt(cb.value); + const checkboxValue = cb.value; if ( associatedResourceIds.includes( checkboxValue, @@ -6390,6 +6422,39 @@ if (window.htmx && !window._resourcesHtmxHandlerAttached) { ); } } + + // If we're in the Add Server resources container, restore persisted selections + else if (container.id === "associatedResources") { + try { + const dataAttr = container.getAttribute( + "data-selected-resources", + ); + if (dataAttr) { + const selectedIds = JSON.parse(dataAttr); + if ( + Array.isArray(selectedIds) && + selectedIds.length > 0 + ) { + newCheckboxes.forEach((cb) => { + if (selectedIds.includes(cb.value)) { + cb.checked = true; + } + cb.removeAttribute("data-auto-check"); + }); + + const event = new Event("change", { + bubbles: true, + }); + container.dispatchEvent(event); + } + } + } catch (e) { + console.warn( + "Error restoring associatedResources selections:", + e, + ); + } + } } }, 10); } @@ -6470,7 +6535,7 @@ if (window.htmx && !window._promptsHtmxHandlerAttached) { try { const associatedPromptIds = JSON.parse(dataAttr); newCheckboxes.forEach((cb) => { - const checkboxValue = parseInt(cb.value); + const checkboxValue = cb.value; if ( associatedPromptIds.includes(checkboxValue) ) { @@ -6492,6 +6557,39 @@ if (window.htmx && !window._promptsHtmxHandlerAttached) { ); } } + + // If we're in the Add Server prompts container, restore persisted selections + else if (container.id === "associatedPrompts") { + try { + const dataAttr = container.getAttribute( + "data-selected-prompts", + ); + if (dataAttr) { + const selectedIds = JSON.parse(dataAttr); + if ( + Array.isArray(selectedIds) && + selectedIds.length > 0 + ) { + newCheckboxes.forEach((cb) => { + if (selectedIds.includes(cb.value)) { + cb.checked = true; + } + cb.removeAttribute("data-auto-check"); + }); + + const event = new Event("change", { + bubbles: true, + }); + container.dispatchEvent(event); + } + } + } catch (e) { + console.warn( + "Error restoring associatedPrompts selections:", + e, + ); + } + } } }, 10); } @@ -7341,6 +7439,45 @@ function initToolSelect( return; } + // Instrument changes to the data-selected-tools attribute for debugging + if (!container.dataset.attrObserverAttached) { + try { + const attrObserver = new MutationObserver((mutationsList) => { + for (const mut of mutationsList) { + if ( + mut.type === "attributes" && + mut.attributeName === "data-selected-tools" + ) { + const oldVal = mut.oldValue; + const newVal = container.getAttribute( + "data-selected-tools", + ); + console.info( + `[DATA-INSTRUMENT] ${selectId} data-selected-tools changed — old: ${oldVal} new: ${newVal}`, + ); + } + } + }); + + // Observe attribute changes and capture previous value + attrObserver.observe(container, { + attributes: true, + attributeOldValue: true, + attributeFilter: ["data-selected-tools"], + }); + + // Prevent double attaching + container.dataset.attrObserverAttached = "true"; + // Keep a reference so it doesn't get GC'd (and so we could disconnect if needed) + container._dbgAttrObserver = attrObserver; + } catch (e) { + console.error( + "[DATA-INSTRUMENT] failed to attach attribute observer:", + e, + ); + } + } + const pillClasses = "inline-block bg-green-100 text-green-800 text-xs px-2 py-1 rounded-full dark:bg-green-900 dark:text-green-200"; @@ -7374,7 +7511,27 @@ function initToolSelect( } } + // Get persisted selections for Add Server mode + let persistedToolIds = []; + if (selectId === "associatedTools") { + const dataAttr = container.getAttribute("data-selected-tools"); + if (dataAttr) { + try { + persistedToolIds = JSON.parse(dataAttr); + } catch (e) { + console.error("Error parsing data-selected-tools:", e); + } + } + if ( + (!persistedToolIds || persistedToolIds.length === 0) && + Array.isArray(window._selectedAssociatedTools) + ) { + persistedToolIds = window._selectedAssociatedTools.slice(); + } + } + let count = checked.length; + const pillsData = []; // If Select All mode is active, use the count from allToolIds if ( @@ -7397,35 +7554,68 @@ function initToolSelect( ) { count = serverTools.length; } + // If in Add Server mode with persisted selections, use persisted count and build pills from persisted data + else if ( + selectId === "associatedTools" && + persistedToolIds && + persistedToolIds.length > 0 + ) { + count = persistedToolIds.length; + // Build pill data from persisted IDs using toolMapping + if (window.toolMapping) { + persistedToolIds.forEach((id) => { + const toolName = window.toolMapping[id]; + if (toolName) { + pillsData.push({ id, name: toolName }); + } + }); + } + } // Rebuild pills safely - show first 3, then summarize the rest pillsBox.innerHTML = ""; const maxPillsToShow = 3; - // In edit server mode, we want to show the server tools rather than just currently checked ones - let pillsToDisplay = checked; - if ( + // Determine which pills to display based on mode + if (selectId === "associatedTools" && pillsData.length > 0) { + // In Add Server mode with persisted data, show pills from persisted selections + pillsData.slice(0, maxPillsToShow).forEach((item) => { + const span = document.createElement("span"); + span.className = pillClasses; + span.textContent = item.name || "Unnamed"; + span.title = item.name; + pillsBox.appendChild(span); + }); + } else if ( isEditServerMode && serverTools && Array.isArray(serverTools) && window.toolMapping ) { - // Create a list of tools that exist both in serverTools and currently loaded tools + // In edit server mode, show the server tools rather than just currently checked ones const allLoadedTools = Array.from(checkboxes); - pillsToDisplay = allLoadedTools.filter((checkbox) => { + const pillsToDisplay = allLoadedTools.filter((checkbox) => { const toolName = window.toolMapping[checkbox.value]; return toolName && serverTools.includes(toolName); }); + pillsToDisplay.slice(0, maxPillsToShow).forEach((cb) => { + const span = document.createElement("span"); + span.className = pillClasses; + span.textContent = + cb.nextElementSibling?.textContent?.trim() || "Unnamed"; + pillsBox.appendChild(span); + }); + } else { + // Default: show pills from currently checked checkboxes + checked.slice(0, maxPillsToShow).forEach((cb) => { + const span = document.createElement("span"); + span.className = pillClasses; + span.textContent = + cb.nextElementSibling?.textContent?.trim() || "Unnamed"; + pillsBox.appendChild(span); + }); } - pillsToDisplay.slice(0, maxPillsToShow).forEach((cb) => { - const span = document.createElement("span"); - span.className = pillClasses; - span.textContent = - cb.nextElementSibling?.textContent?.trim() || "Unnamed"; - pillsBox.appendChild(span); - }); - // If more than maxPillsToShow, show a summary pill if (count > maxPillsToShow) { const span = document.createElement("span"); @@ -7661,6 +7851,82 @@ function initToolSelect( ); } } + // If we're in the Add Server tools container, persist selected IDs + else if (selectId === "associatedTools") { + try { + // Incrementally update persisted selection instead of + // replacing it wholesale. This preserves selections made + // in previous filtered views where those checkboxes are + // not present in the current DOM. + const changedEl = e.target; + const changedId = changedEl.value; + + // Load existing persisted set: prefer container attribute, + // fall back to the in-memory window variable. + let persisted = []; + const dataAttr = container.getAttribute( + "data-selected-tools", + ); + if (dataAttr) { + try { + const parsed = JSON.parse(dataAttr); + if (Array.isArray(parsed)) { + persisted = parsed.slice(); + } + } catch (parseErr) { + console.error( + "Error parsing existing data-selected-tools:", + parseErr, + ); + } + } else if ( + Array.isArray(window._selectedAssociatedTools) + ) { + persisted = window._selectedAssociatedTools.slice(); + } + + if (changedEl.checked) { + if (!persisted.includes(changedId)) { + persisted.push(changedId); + } + } else { + persisted = persisted.filter( + (x) => x !== changedId, + ); + } + + // Ensure any currently visible checked boxes are included + const visibleChecked = Array.from( + container.querySelectorAll( + 'input[type="checkbox"]:checked', + ), + ).map((cb) => cb.value); + visibleChecked.forEach((id) => { + if (!persisted.includes(id)) { + persisted.push(id); + } + }); + + // Persist back to both the container attribute and global fallback + container.setAttribute( + "data-selected-tools", + JSON.stringify(persisted), + ); + try { + window._selectedAssociatedTools = persisted.slice(); + } catch (e) { + console.error( + "Error persisting window._selectedAssociatedTools:", + e, + ); + } + } catch (err) { + console.error( + "Error updating data-selected-tools (incremental):", + err, + ); + } + } update(); } @@ -7698,7 +7964,6 @@ function initResourceSelect( 'input[type="checkbox"]', ); const checked = Array.from(checkboxes).filter((cb) => cb.checked); - // const count = checked.length; // Select All handling const selectAllInput = container.querySelector( @@ -7708,7 +7973,35 @@ function initResourceSelect( 'input[name="allResourceIds"]', ); + // Get persisted selections for Add Server mode + let persistedResourceIds = []; + if (selectId === "associatedResources") { + const dataAttr = container.getAttribute( + "data-selected-resources", + ); + if (dataAttr) { + try { + persistedResourceIds = JSON.parse(dataAttr); + } catch (e) { + console.error( + "Error parsing data-selected-resources:", + e, + ); + } + } + if ( + (!persistedResourceIds || + persistedResourceIds.length === 0) && + Array.isArray(window._selectedAssociatedResources) + ) { + persistedResourceIds = + window._selectedAssociatedResources.slice(); + } + } + let count = checked.length; + const pillsData = []; + if ( selectAllInput && selectAllInput.value === "true" && @@ -7721,18 +8014,51 @@ function initResourceSelect( console.error("Error parsing allResourceIds:", e); } } + // If in Add Server mode with persisted selections, use persisted count and build pills from persisted data + else if ( + selectId === "associatedResources" && + persistedResourceIds && + persistedResourceIds.length > 0 + ) { + count = persistedResourceIds.length; + // Build pill data from persisted IDs - find matching checkboxes or use ID as fallback + const checkboxMap = new Map(); + checkboxes.forEach((cb) => { + checkboxMap.set( + cb.value, + cb.nextElementSibling?.textContent?.trim() || cb.value, + ); + }); + persistedResourceIds.forEach((id) => { + const name = checkboxMap.get(id) || id; + pillsData.push({ id, name }); + }); + } // Rebuild pills safely - show first 3, then summarize the rest 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); - }); + // Determine which pills to display based on mode + if (selectId === "associatedResources" && pillsData.length > 0) { + // In Add Server mode with persisted data, show pills from persisted selections + pillsData.slice(0, maxPillsToShow).forEach((item) => { + const span = document.createElement("span"); + span.className = pillClasses; + span.textContent = item.name || "Unnamed"; + span.title = item.name; + pillsBox.appendChild(span); + }); + } else { + // Default: show pills from currently checked checkboxes + 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 more than maxPillsToShow, show a summary pill if (count > maxPillsToShow) { @@ -7937,7 +8263,7 @@ function initResourceSelect( } } - const idVal = parseInt(e.target.value); + const idVal = e.target.value; if (!Number.isNaN(idVal)) { if (e.target.checked) { if (!serverResources.includes(idVal)) { @@ -7961,6 +8287,76 @@ function initResourceSelect( ); } } + // If we're in the Add Server resources container, persist selected IDs incrementally + else if (selectId === "associatedResources") { + try { + const changedEl = e.target; + const changedId = changedEl.value; + + let persisted = []; + const dataAttr = container.getAttribute( + "data-selected-resources", + ); + if (dataAttr) { + try { + const parsed = JSON.parse(dataAttr); + if (Array.isArray(parsed)) { + persisted = parsed.slice(); + } + } catch (parseErr) { + console.error( + "Error parsing existing data-selected-resources:", + parseErr, + ); + } + } else if ( + Array.isArray(window._selectedAssociatedResources) + ) { + persisted = + window._selectedAssociatedResources.slice(); + } + + if (changedEl.checked) { + if (!persisted.includes(changedId)) { + persisted.push(changedId); + } + } else { + persisted = persisted.filter( + (x) => x !== changedId, + ); + } + + const visibleChecked = Array.from( + container.querySelectorAll( + 'input[type="checkbox"]:checked', + ), + ).map((cb) => cb.value); + visibleChecked.forEach((id) => { + if (!persisted.includes(id)) { + persisted.push(id); + } + }); + + container.setAttribute( + "data-selected-resources", + JSON.stringify(persisted), + ); + try { + window._selectedAssociatedResources = + persisted.slice(); + } catch (err) { + console.error( + "Error persisting window._selectedAssociatedResources:", + err, + ); + } + } catch (err) { + console.error( + "Error updating data-selected-resources (incremental):", + err, + ); + } + } update(); } @@ -8007,7 +8403,34 @@ function initPromptSelect( 'input[name="allPromptIds"]', ); + // Get persisted selections for Add Server mode + let persistedPromptIds = []; + if (selectId === "associatedPrompts") { + const dataAttr = container.getAttribute( + "data-selected-prompts", + ); + if (dataAttr) { + try { + persistedPromptIds = JSON.parse(dataAttr); + } catch (e) { + console.error( + "Error parsing data-selected-prompts:", + e, + ); + } + } + if ( + (!persistedPromptIds || persistedPromptIds.length === 0) && + Array.isArray(window._selectedAssociatedPrompts) + ) { + persistedPromptIds = + window._selectedAssociatedPrompts.slice(); + } + } + let count = checked.length; + const pillsData = []; + if ( selectAllInput && selectAllInput.value === "true" && @@ -8020,18 +8443,51 @@ function initPromptSelect( console.error("Error parsing allPromptIds:", e); } } + // If in Add Server mode with persisted selections, use persisted count and build pills from persisted data + else if ( + selectId === "associatedPrompts" && + persistedPromptIds && + persistedPromptIds.length > 0 + ) { + count = persistedPromptIds.length; + // Build pill data from persisted IDs - find matching checkboxes or use ID as fallback + const checkboxMap = new Map(); + checkboxes.forEach((cb) => { + checkboxMap.set( + cb.value, + cb.nextElementSibling?.textContent?.trim() || cb.value, + ); + }); + persistedPromptIds.forEach((id) => { + const name = checkboxMap.get(id) || id; + pillsData.push({ id, name }); + }); + } // Rebuild pills safely - show first 3, then summarize the rest 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); - }); + // Determine which pills to display based on mode + if (selectId === "associatedPrompts" && pillsData.length > 0) { + // In Add Server mode with persisted data, show pills from persisted selections + pillsData.slice(0, maxPillsToShow).forEach((item) => { + const span = document.createElement("span"); + span.className = pillClasses; + span.textContent = item.name || "Unnamed"; + span.title = item.name; + pillsBox.appendChild(span); + }); + } else { + // Default: show pills from currently checked checkboxes + 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 more than maxPillsToShow, show a summary pill if (count > maxPillsToShow) { @@ -8235,7 +8691,7 @@ function initPromptSelect( } } - const idVal = parseInt(e.target.value); + const idVal = e.target.value; if (!Number.isNaN(idVal)) { if (e.target.checked) { if (!serverPrompts.includes(idVal)) { @@ -8260,36 +8716,107 @@ function initPromptSelect( } } - update(); - } - }); - } -} - -// =================================================================== -// GATEWAY SELECT (Associated MCP Servers) - search/select/clear -// =================================================================== -function initGatewaySelect( - selectId = "associatedGateways", - pillsId = "selectedGatewayPills", - warnId = "selectedGatewayWarning", - max = 12, - selectBtnId = "selectAllGatewayBtn", - clearBtnId = "clearAllGatewayBtn", - searchInputId = "searchGateways", -) { - 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; - const searchInput = searchInputId - ? document.getElementById(searchInputId) - : null; + // If we're in the Add Server prompts container, persist selected IDs incrementally + else if (selectId === "associatedPrompts") { + try { + const changedEl = e.target; + const changedId = changedEl.value; - if (!container || !pillsBox || !warnBox) { - console.warn( - `Gateway select elements not found: ${selectId}, ${pillsId}, ${warnId}`, + let persisted = []; + const dataAttr = container.getAttribute( + "data-selected-prompts", + ); + if (dataAttr) { + try { + const parsed = JSON.parse(dataAttr); + if (Array.isArray(parsed)) { + persisted = parsed.slice(); + } + } catch (parseErr) { + console.error( + "Error parsing existing data-selected-prompts:", + parseErr, + ); + } + } else if ( + Array.isArray(window._selectedAssociatedPrompts) + ) { + persisted = + window._selectedAssociatedPrompts.slice(); + } + + if (changedEl.checked) { + if (!persisted.includes(changedId)) { + persisted.push(changedId); + } + } else { + persisted = persisted.filter( + (x) => x !== changedId, + ); + } + + const visibleChecked = Array.from( + container.querySelectorAll( + 'input[type="checkbox"]:checked', + ), + ).map((cb) => cb.value); + visibleChecked.forEach((id) => { + if (!persisted.includes(id)) { + persisted.push(id); + } + }); + + container.setAttribute( + "data-selected-prompts", + JSON.stringify(persisted), + ); + try { + window._selectedAssociatedPrompts = + persisted.slice(); + } catch (err) { + console.error( + "Error persisting window._selectedAssociatedPrompts:", + err, + ); + } + } catch (err) { + console.error( + "Error updating data-selected-prompts (incremental):", + err, + ); + } + } + + update(); + } + }); + } +} + +// =================================================================== +// GATEWAY SELECT (Associated MCP Servers) - search/select/clear +// =================================================================== +function initGatewaySelect( + selectId = "associatedGateways", + pillsId = "selectedGatewayPills", + warnId = "selectedGatewayWarning", + max = 12, + selectBtnId = "selectAllGatewayBtn", + clearBtnId = "clearAllGatewayBtn", + searchInputId = "searchGateways", +) { + 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; + const searchInput = searchInputId + ? document.getElementById(searchInputId) + : null; + + if (!container || !pillsBox || !warnBox) { + console.warn( + `Gateway select elements not found: ${selectId}, ${pillsId}, ${warnId}`, ); return; } @@ -8853,7 +9380,59 @@ function reloadAssociatedItems() { "[Filter Update DEBUG] Resources fetch successful, HTML length:", html.length, ); + // Persist current selections to window fallback before replacing container + // AND preserve the data-selected-resources attribute + let persistedResourceIds = []; + try { + // First, try to get from the container's data attribute + const dataAttr = resourcesContainer.getAttribute( + "data-selected-resources", + ); + if (dataAttr) { + try { + const parsed = JSON.parse(dataAttr); + if (Array.isArray(parsed)) { + persistedResourceIds = parsed.slice(); + } + } catch (e) { + console.error( + "Error parsing data-selected-resources:", + e, + ); + } + } + + // Merge with currently checked items + const currentChecked = Array.from( + resourcesContainer.querySelectorAll( + 'input[type="checkbox"]:checked', + ), + ).map((cb) => cb.value); + const merged = new Set([ + ...persistedResourceIds, + ...currentChecked, + ]); + persistedResourceIds = Array.from(merged); + + // Update window fallback + window._selectedAssociatedResources = + persistedResourceIds.slice(); + } catch (e) { + console.error( + "Error capturing current resource selections before reload:", + e, + ); + } + resourcesContainer.innerHTML = html; + + // Immediately restore the data-selected-resources attribute after innerHTML replacement + if (persistedResourceIds.length > 0) { + resourcesContainer.setAttribute( + "data-selected-resources", + JSON.stringify(persistedResourceIds), + ); + } // If HTMX is available, process the newly-inserted HTML so hx-* // triggers (like the infinite-scroll 'intersect' trigger) are // initialized. To avoid HTMX re-triggering the container's @@ -8921,6 +9500,91 @@ function reloadAssociatedItems() { ? "clearAllEditResourcesBtn" : "clearAllResourcesBtn"; + // The data-selected-resources attribute should already be restored above, + // but double-check and merge with window fallback if needed + try { + const dataAttr = resourcesContainer.getAttribute( + "data-selected-resources", + ); + let selectedIds = []; + if (dataAttr) { + try { + const parsed = JSON.parse(dataAttr); + if (Array.isArray(parsed)) { + selectedIds = parsed.slice(); + } + } catch (e) { + console.error( + "Error parsing data-selected-resources:", + e, + ); + } + } + + // Merge with window fallback if it has additional selections + if ( + Array.isArray(window._selectedAssociatedResources) && + window._selectedAssociatedResources.length > 0 + ) { + const merged = new Set([ + ...selectedIds, + ...window._selectedAssociatedResources, + ]); + const mergedArray = Array.from(merged); + if (mergedArray.length > selectedIds.length) { + resourcesContainer.setAttribute( + "data-selected-resources", + JSON.stringify(mergedArray), + ); + console.log( + "[Filter Update DEBUG] Merged additional selections from window fallback", + ); + } + } + } catch (e) { + console.error( + "Error restoring data-selected-resources after fetch reload:", + e, + ); + } + + // First restore persisted selections from data-selected-resources (Add Server mode) + try { + const dataAttr = resourcesContainer.getAttribute( + "data-selected-resources", + ); + if ( + dataAttr && + resourcesContainerId === "associatedResources" + ) { + const selectedIds = JSON.parse(dataAttr); + if ( + Array.isArray(selectedIds) && + selectedIds.length > 0 + ) { + const resourceCheckboxes = + resourcesContainer.querySelectorAll( + 'input[type="checkbox"][name="associatedResources"]', + ); + resourceCheckboxes.forEach((cb) => { + if (selectedIds.includes(cb.value)) { + cb.checked = true; + } + }); + console.log( + "[Filter Update DEBUG] Restored", + selectedIds.length, + "persisted resource selections", + ); + } + } + } catch (e) { + console.warn( + "Error restoring persisted resource selections:", + e, + ); + } + initResourceSelect( resourcesContainerId, resPills, @@ -8929,9 +9593,10 @@ function reloadAssociatedItems() { resSelectBtn, resClearBtn, ); + // Re-apply server-associated resource selections so selections - // persist across gateway-filtered reloads. The resources partial - // replaces checkbox inputs; use the container's + // persist across gateway-filtered reloads (Edit Server mode). + // The resources partial replaces checkbox inputs; use the container's // `data-server-resources` attribute (set when opening edit modal) // to restore checked state. try { @@ -8949,7 +9614,7 @@ function reloadAssociatedItems() { 'input[type="checkbox"][name="associatedResources"]', ); resourceCheckboxes.forEach((cb) => { - const val = parseInt(cb.value); + const val = cb.value; if ( !Number.isNaN(val) && associated.includes(val) @@ -8989,11 +9654,91 @@ function reloadAssociatedItems() { ? `${window.ROOT_PATH}/admin/prompts/partial?page=1&per_page=50&render=selector&gateway_id=${encodeURIComponent(gatewayIdParam)}` : `${window.ROOT_PATH}/admin/prompts/partial?page=1&per_page=50&render=selector`; + // Persist current prompt selections before HTMX replaces the container + try { + const currentCheckedPrompts = Array.from( + promptsContainer.querySelectorAll( + 'input[type="checkbox"]:checked', + ), + ).map((cb) => cb.value); + if ( + !Array.isArray(window._selectedAssociatedPrompts) || + window._selectedAssociatedPrompts.length === 0 + ) { + window._selectedAssociatedPrompts = + currentCheckedPrompts.slice(); + } else { + const merged = new Set([ + ...(window._selectedAssociatedPrompts || []), + ...currentCheckedPrompts, + ]); + window._selectedAssociatedPrompts = Array.from(merged); + } + } catch (e) { + console.error( + "Error capturing current prompt selections before reload:", + e, + ); + } + if (window.htmx) { htmx.ajax("GET", promptsUrl, { target: `#${promptsContainerId}`, swap: "innerHTML", }).then(() => { + try { + const containerEl = + document.getElementById(promptsContainerId); + if (containerEl) { + const existingAttr = containerEl.getAttribute( + "data-selected-prompts", + ); + let existingIds = null; + if (existingAttr) { + try { + existingIds = JSON.parse(existingAttr); + } catch (e) { + console.error( + "Error parsing existing data-selected-prompts after reload:", + e, + ); + } + } + + if ( + (!existingIds || + !Array.isArray(existingIds) || + existingIds.length === 0) && + Array.isArray(window._selectedAssociatedPrompts) && + window._selectedAssociatedPrompts.length > 0 + ) { + containerEl.setAttribute( + "data-selected-prompts", + JSON.stringify( + window._selectedAssociatedPrompts.slice(), + ), + ); + } else if ( + Array.isArray(existingIds) && + Array.isArray(window._selectedAssociatedPrompts) && + window._selectedAssociatedPrompts.length > 0 + ) { + const merged = new Set([ + ...(existingIds || []), + ...window._selectedAssociatedPrompts, + ]); + containerEl.setAttribute( + "data-selected-prompts", + JSON.stringify(Array.from(merged)), + ); + } + } + } catch (e) { + console.error( + "Error restoring data-selected-prompts after HTMX reload:", + e, + ); + } // Re-initialize the prompt select after content is loaded const pPills = useEditContainers ? "selectedEditPromptsPills" @@ -19568,8 +20313,8 @@ function resetImportSelection() { // Many panels use specific ids — attempt to call generic initializers if they exist initResourceSelect( "associatedResources", - "resource-pills", - "resource-warn", + "selectedResourcePills", + "selectedResourceWarning", 10, null, null, @@ -19578,8 +20323,8 @@ function resetImportSelection() { if (typeof initToolSelect === "function") { initToolSelect( "associatedTools", - "tool-pills", - "tool-warn", + "selectedToolsPills", + "selectedToolsWarning", 10, null, null, @@ -20447,6 +21192,25 @@ function initializeLLMChat() { // Generate or retrieve user ID llmChatState.userId = generateUserId(); + // Restore previously selected server (if any) from sessionStorage + try { + const persistedServerId = sessionStorage.getItem( + "llm_chat_selected_server_id", + ); + const persistedServerName = sessionStorage.getItem( + "llm_chat_selected_server_name", + ); + if (persistedServerId) { + llmChatState.selectedServerId = persistedServerId; + if (persistedServerName) { + llmChatState.selectedServerName = persistedServerName; + } + } + } catch (e) { + // sessionStorage may be unavailable in some environments + console.warn("Could not restore persisted LLM server selection:", e); + } + // Load servers if not already loaded const serversList = document.getElementById("llm-chat-servers-list"); if (serversList && serversList.children.length <= 1) { @@ -20633,6 +21397,15 @@ async function selectServerForChat( llmChatState.selectedServerId = serverId; llmChatState.selectedServerName = serverName; + // Persist selection so it survives tab reloads within the session + try { + sessionStorage.setItem("llm_chat_selected_server_id", serverId); + sessionStorage.setItem("llm_chat_selected_server_name", serverName); + } catch (e) { + // sessionStorage may be unavailable (e.g. privacy mode); ignore silently + console.warn("Could not persist selected LLM server:", e); + } + // Update UI to show selected server const serverItems = document.querySelectorAll(".server-item"); serverItems.forEach((item) => { @@ -22260,6 +23033,109 @@ async function serverSideToolSearch(searchTerm) { return; } + // Get selected gateway IDs to maintain filtering + const selectedGatewayIds = getSelectedGatewayIds + ? getSelectedGatewayIds() + : []; + const gatewayIdParam = + selectedGatewayIds.length > 0 ? selectedGatewayIds.join(",") : ""; + + console.log( + `[Tool Search] Searching with gateway filter: ${gatewayIdParam || "none (showing all)"}`, + ); + + // --- DOM instrumentation for debugging replacement during searches --- + // Assign a stable debug id to the container (persists through innerHTML swaps + // but will change if the element is replaced). Observe the parent node for + // childList mutations and log if the container is removed or replaced. + let _domInstrObserver = null; + let _domInstrId = null; + try { + if (!container.dataset.debugNodeId) { + container.dataset.debugNodeId = `dbg-${Date.now()}-${Math.floor(Math.random() * 10000)}`; + } + _domInstrId = container.dataset.debugNodeId; + console.info( + `[DOM-INSTRUMENT] serverSideToolSearch start for #associatedTools debugId=${_domInstrId} searchTerm='${searchTerm}'`, + ); + + const parentNode = container.parentNode; + if (parentNode) { + _domInstrObserver = new MutationObserver((mutationsList) => { + for (const mut of mutationsList) { + if (mut.type === "childList") { + const current = + document.getElementById("associatedTools"); + if (!current) { + console.warn( + `[DOM-INSTRUMENT] associatedTools element REMOVED during search (original debugId=${_domInstrId})`, + mut, + ); + } else { + const curId = current.dataset.debugNodeId || null; + if (curId !== _domInstrId) { + console.warn( + `[DOM-INSTRUMENT] associatedTools element REPLACED during search. original=${_domInstrId} current=${curId}`, + mut, + ); + } + } + } + } + }); + try { + _domInstrObserver.observe(parentNode, { childList: true }); + } catch (e) { + console.error( + "[DOM-INSTRUMENT] Failed to observe parent node for associatedTools:", + e, + ); + } + } + } catch (e) { + console.error("[DOM-INSTRUMENT] setup error:", e); + } + + // Persist current selections to window fallback AND data attribute before we replace/clear the container + let persistedToolIds = []; + try { + // First get from data attribute if it exists + const dataAttr = container.getAttribute("data-selected-tools"); + if (dataAttr) { + try { + const parsed = JSON.parse(dataAttr); + if (Array.isArray(parsed)) { + persistedToolIds = parsed.slice(); + } + } catch (e) { + console.error("Error parsing data-selected-tools:", e); + } + } + + // Then merge with currently checked items (important for search results) + const currentChecked = Array.from( + container.querySelectorAll('input[type="checkbox"]:checked'), + ).map((cb) => cb.value); + const merged = new Set([...persistedToolIds, ...currentChecked]); + persistedToolIds = Array.from(merged); + + // Update both the window fallback and the container attribute + window._selectedAssociatedTools = persistedToolIds.slice(); + if (persistedToolIds.length > 0) { + container.setAttribute( + "data-selected-tools", + JSON.stringify(persistedToolIds), + ); + } + + console.log( + `[Tool Search] Persisted ${persistedToolIds.length} tool selections before search:`, + persistedToolIds, + ); + } catch (e) { + console.error("Error capturing current selections before search:", e); + } + // Show loading state container.innerHTML = `
@@ -22272,26 +23148,145 @@ async function serverSideToolSearch(searchTerm) { `; if (searchTerm.trim() === "") { - // If search term is empty, reload the default tool list + // If search term is empty, reload the default tool list with gateway filter try { - const response = await fetch( - `${window.ROOT_PATH}/admin/tools/partial?page=1&per_page=50&render=selector`, + const toolsUrl = gatewayIdParam + ? `${window.ROOT_PATH}/admin/tools/partial?page=1&per_page=50&render=selector&gateway_id=${encodeURIComponent(gatewayIdParam)}` + : `${window.ROOT_PATH}/admin/tools/partial?page=1&per_page=50&render=selector`; + + console.log( + `[Tool Search] Loading default tools with URL: ${toolsUrl}`, ); + + const response = await fetch(toolsUrl); if (response.ok) { const html = await response.text(); + + // Preserve the data-selected-tools attribute before replacing innerHTML + let persistedToolIds = []; + try { + const dataAttr = container.getAttribute( + "data-selected-tools", + ); + if (dataAttr) { + try { + const parsed = JSON.parse(dataAttr); + if (Array.isArray(parsed)) { + persistedToolIds = parsed.slice(); + } + } catch (e) { + console.error( + "Error parsing data-selected-tools before clearing search:", + e, + ); + } + } + + // Merge with currently checked items + const currentChecked = Array.from( + container.querySelectorAll( + 'input[type="checkbox"]:checked', + ), + ).map((cb) => cb.value); + const merged = new Set([ + ...persistedToolIds, + ...currentChecked, + ]); + persistedToolIds = Array.from(merged); + + // Update window fallback + window._selectedAssociatedTools = persistedToolIds.slice(); + } catch (e) { + console.error( + "Error capturing current tool selections before clearing search:", + e, + ); + } + container.innerHTML = html; - // Hide no results message - if (noResultsMessage) { - noResultsMessage.style.display = "none"; + // Immediately restore the data-selected-tools attribute after innerHTML replacement + if (persistedToolIds.length > 0) { + container.setAttribute( + "data-selected-tools", + JSON.stringify(persistedToolIds), + ); } - // Update tool mapping if needed - updateToolMapping(container); - } else { - container.innerHTML = - '
Failed to load tools
'; - } + // If the container has been re-rendered server-side and our + // `data-selected-tools` attribute was lost, restore from the + // global fallback `window._selectedAssociatedTools`. + try { + updateToolMapping(container); + + // Re-initialize selector so handlers are attached + initToolSelect( + "associatedTools", + "selectedToolsPills", + "selectedToolsWarning", + 6, + "selectAllToolsBtn", + "clearAllToolsBtn", + ); + + const dataAttr = container.getAttribute( + "data-selected-tools", + ); + let selectedIds = null; + if (dataAttr) { + try { + selectedIds = JSON.parse(dataAttr); + } catch (e) { + console.error( + "Error parsing server data-selected-tools:", + e, + ); + } + } + + if ( + (!selectedIds || + !Array.isArray(selectedIds) || + selectedIds.length === 0) && + Array.isArray(window._selectedAssociatedTools) + ) { + selectedIds = window._selectedAssociatedTools.slice(); + } + + if (Array.isArray(selectedIds) && selectedIds.length > 0) { + const checkboxes = container.querySelectorAll( + 'input[name="associatedTools"]', + ); + checkboxes.forEach((cb) => { + if (selectedIds.includes(cb.value)) { + cb.checked = true; + } + }); + + const firstCb = container.querySelector( + 'input[type="checkbox"]', + ); + if (firstCb) { + firstCb.dispatchEvent( + new Event("change", { bubbles: true }), + ); + } + } + + // Hide no results message + if (noResultsMessage) { + noResultsMessage.style.display = "none"; + } + } catch (e) { + console.error( + "Error restoring selections after loading default tools:", + e, + ); + } + } else { + container.innerHTML = + '
Failed to load tools
'; + } } catch (error) { console.error("Error loading tools:", error); container.innerHTML = @@ -22301,10 +23296,14 @@ async function serverSideToolSearch(searchTerm) { } try { - // Call the new search API - const response = await fetch( - `${window.ROOT_PATH}/admin/tools/search?q=${encodeURIComponent(searchTerm)}&limit=100`, - ); + // Call the search API with gateway filter + const searchUrl = gatewayIdParam + ? `${window.ROOT_PATH}/admin/tools/search?q=${encodeURIComponent(searchTerm)}&limit=100&gateway_id=${encodeURIComponent(gatewayIdParam)}` + : `${window.ROOT_PATH}/admin/tools/search?q=${encodeURIComponent(searchTerm)}&limit=100`; + + console.log(`[Tool Search] Searching tools with URL: ${searchUrl}`); + + const response = await fetch(searchUrl); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); @@ -22342,10 +23341,135 @@ async function serverSideToolSearch(searchTerm) { }); container.innerHTML = searchResultsHtml; + // If server-side didn't provide `data-selected-tools` (or provided + // an empty array), restore/merge from the in-memory fallback so + // the attribute isn't left empty and selectors can pick it up. + try { + const existingAttr = container.getAttribute( + "data-selected-tools", + ); + let existingIds = null; + if (existingAttr) { + try { + existingIds = JSON.parse(existingAttr); + } catch (e) { + console.error( + "Error parsing existing data-selected-tools after search insert:", + e, + ); + } + } + + if ( + (!existingIds || + !Array.isArray(existingIds) || + existingIds.length === 0) && + Array.isArray(window._selectedAssociatedTools) && + window._selectedAssociatedTools.length > 0 + ) { + // Write a merged view back to the container attribute so + // subsequent init/observers see the selection + container.setAttribute( + "data-selected-tools", + JSON.stringify(window._selectedAssociatedTools.slice()), + ); + } else if ( + Array.isArray(existingIds) && + Array.isArray(window._selectedAssociatedTools) && + window._selectedAssociatedTools.length > 0 + ) { + // Merge the two sets to avoid losing either + const merged = new Set([ + ...(existingIds || []), + ...window._selectedAssociatedTools, + ]); + container.setAttribute( + "data-selected-tools", + JSON.stringify(Array.from(merged)), + ); + } + } catch (e) { + console.error( + "Error restoring data-selected-tools attribute after inserting search results:", + e, + ); + } // Update tool mapping with search results updateToolMapping(container); + // Re-initialize selector behavior for the add-server container + try { + initToolSelect( + "associatedTools", + "selectedToolsPills", + "selectedToolsWarning", + 6, + "selectAllToolsBtn", + "clearAllToolsBtn", + ); + + // Restore any previously selected tool IDs stored on the container + try { + const dataAttr = container.getAttribute( + "data-selected-tools", + ); + let selectedIds = null; + if (dataAttr) { + try { + selectedIds = JSON.parse(dataAttr); + } catch (e) { + console.error( + "Error parsing data-selected-tools:", + e, + ); + } + } + + // If parsed attribute is missing or an empty array, fall back + // to the in-memory `window._selectedAssociatedTools` saved earlier. + if ( + (!selectedIds || + !Array.isArray(selectedIds) || + selectedIds.length === 0) && + Array.isArray(window._selectedAssociatedTools) + ) { + selectedIds = window._selectedAssociatedTools.slice(); + } + + if (Array.isArray(selectedIds) && selectedIds.length > 0) { + const checkboxes = container.querySelectorAll( + 'input[name="associatedTools"]', + ); + checkboxes.forEach((cb) => { + if (selectedIds.includes(cb.value)) { + cb.checked = true; + } + }); + + // Trigger update so pills/counts refresh + const firstCb = container.querySelector( + 'input[type="checkbox"]', + ); + if (firstCb) { + firstCb.dispatchEvent( + new Event("change", { bubbles: true }), + ); + } + } + } catch (e) { + console.error( + "Error restoring data-selected-tools after search:", + e, + ); + } + } catch (e) { + console.error( + "Error initializing associatedTools selector:", + e, + ); + } + // Hide no results message if (noResultsMessage) { noResultsMessage.style.display = "none"; @@ -22451,6 +23575,41 @@ async function serverSidePromptSearch(searchTerm) { return; } + // Get selected gateway IDs to maintain filtering + const selectedGatewayIds = getSelectedGatewayIds + ? getSelectedGatewayIds() + : []; + const gatewayIdParam = + selectedGatewayIds.length > 0 ? selectedGatewayIds.join(",") : ""; + + console.log( + `[Prompt Search] Searching with gateway filter: ${gatewayIdParam || "none (showing all)"}`, + ); + + // Persist current selections to window fallback before we replace/clear the container + try { + const currentChecked = Array.from( + container.querySelectorAll('input[type="checkbox"]:checked'), + ).map((cb) => cb.value); + if ( + !Array.isArray(window._selectedAssociatedPrompts) || + window._selectedAssociatedPrompts.length === 0 + ) { + window._selectedAssociatedPrompts = currentChecked.slice(); + } else { + const merged = new Set([ + ...(window._selectedAssociatedPrompts || []), + ...currentChecked, + ]); + window._selectedAssociatedPrompts = Array.from(merged); + } + } catch (e) { + console.error( + "Error capturing current prompt selections before search:", + e, + ); + } + // Show loading state container.innerHTML = `
@@ -22463,11 +23622,17 @@ async function serverSidePromptSearch(searchTerm) { `; if (searchTerm.trim() === "") { - // If search term is empty, reload the default prompt selector + // If search term is empty, reload the default prompt selector with gateway filter try { - const response = await fetch( - `${window.ROOT_PATH}/admin/prompts/partial?page=1&per_page=50&render=selector`, + const promptsUrl = gatewayIdParam + ? `${window.ROOT_PATH}/admin/prompts/partial?page=1&per_page=50&render=selector&gateway_id=${encodeURIComponent(gatewayIdParam)}` + : `${window.ROOT_PATH}/admin/prompts/partial?page=1&per_page=50&render=selector`; + + console.log( + `[Prompt Search] Loading default prompts with URL: ${promptsUrl}`, ); + + const response = await fetch(promptsUrl); if (response.ok) { const html = await response.text(); container.innerHTML = html; @@ -22477,15 +23642,67 @@ async function serverSidePromptSearch(searchTerm) { noResultsMessage.style.display = "none"; } - // Initialize prompt mapping if needed - initPromptSelect( - "associatedPrompts", - "selectedPromptsPills", - "selectedPromptsWarning", - 6, - "selectAllPromptsBtn", - "clearAllPromptsBtn", - ); + try { + // Update mapping and ensure persisted selections are applied + // Initialize prompt mapping if needed + // If the server did not supply `data-selected-prompts`, restore from fallback + const dataAttr = container.getAttribute( + "data-selected-prompts", + ); + let selectedIds = null; + if (dataAttr) { + try { + selectedIds = JSON.parse(dataAttr); + } catch (e) { + console.error( + "Error parsing server data-selected-prompts:", + e, + ); + } + } + + if ( + (!selectedIds || + !Array.isArray(selectedIds) || + selectedIds.length === 0) && + Array.isArray(window._selectedAssociatedPrompts) + ) { + selectedIds = window._selectedAssociatedPrompts.slice(); + } + + initPromptSelect( + "associatedPrompts", + "selectedPromptsPills", + "selectedPromptsWarning", + 6, + "selectAllPromptsBtn", + "clearAllPromptsBtn", + ); + + if (Array.isArray(selectedIds) && selectedIds.length > 0) { + const checkboxes = container.querySelectorAll( + 'input[name="associatedPrompts"]', + ); + checkboxes.forEach((cb) => { + if (selectedIds.includes(cb.value)) { + cb.checked = true; + } + }); + const firstCb = container.querySelector( + 'input[type="checkbox"]', + ); + if (firstCb) { + firstCb.dispatchEvent( + new Event("change", { bubbles: true }), + ); + } + } + } catch (e) { + console.error( + "Error restoring selections after loading default prompts:", + e, + ); + } } else { container.innerHTML = '
Failed to load prompts
'; @@ -22499,9 +23716,14 @@ async function serverSidePromptSearch(searchTerm) { } try { - const response = await fetch( - `${window.ROOT_PATH}/admin/prompts/search?q=${encodeURIComponent(searchTerm)}&limit=100`, - ); + // Call the search API with gateway filter + const searchUrl = gatewayIdParam + ? `${window.ROOT_PATH}/admin/prompts/search?q=${encodeURIComponent(searchTerm)}&limit=100&gateway_id=${encodeURIComponent(gatewayIdParam)}` + : `${window.ROOT_PATH}/admin/prompts/search?q=${encodeURIComponent(searchTerm)}&limit=100`; + + console.log(`[Prompt Search] Searching prompts with URL: ${searchUrl}`); + + const response = await fetch(searchUrl); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } @@ -22529,6 +23751,57 @@ async function serverSidePromptSearch(searchTerm) { `; }); + // Before initializing, ensure any persisted selections are merged into the container + try { + const existingAttr = container.getAttribute( + "data-selected-prompts", + ); + let existingIds = null; + if (existingAttr) { + try { + existingIds = JSON.parse(existingAttr); + } catch (e) { + console.error( + "Error parsing existing data-selected-prompts after search insert:", + e, + ); + } + } + + if ( + (!existingIds || + !Array.isArray(existingIds) || + existingIds.length === 0) && + Array.isArray(window._selectedAssociatedPrompts) && + window._selectedAssociatedPrompts.length > 0 + ) { + container.setAttribute( + "data-selected-prompts", + JSON.stringify( + window._selectedAssociatedPrompts.slice(), + ), + ); + } else if ( + Array.isArray(existingIds) && + Array.isArray(window._selectedAssociatedPrompts) && + window._selectedAssociatedPrompts.length > 0 + ) { + const merged = new Set([ + ...(existingIds || []), + ...window._selectedAssociatedPrompts, + ]); + container.setAttribute( + "data-selected-prompts", + JSON.stringify(Array.from(merged)), + ); + } + } catch (e) { + console.error( + "Error restoring data-selected-prompts attribute after inserting search results:", + e, + ); + } + container.innerHTML = searchResultsHtml; // Initialize prompt select mapping @@ -22576,6 +23849,41 @@ async function serverSideResourceSearch(searchTerm) { return; } + // Get selected gateway IDs to maintain filtering + const selectedGatewayIds = getSelectedGatewayIds + ? getSelectedGatewayIds() + : []; + const gatewayIdParam = + selectedGatewayIds.length > 0 ? selectedGatewayIds.join(",") : ""; + + console.log( + `[Resource Search] Searching with gateway filter: ${gatewayIdParam || "none (showing all)"}`, + ); + + // Persist current selections to window fallback before we replace/clear the container + try { + const currentChecked = Array.from( + container.querySelectorAll('input[type="checkbox"]:checked'), + ).map((cb) => cb.value); + if ( + !Array.isArray(window._selectedAssociatedResources) || + window._selectedAssociatedResources.length === 0 + ) { + window._selectedAssociatedResources = currentChecked.slice(); + } else { + const merged = new Set([ + ...(window._selectedAssociatedResources || []), + ...currentChecked, + ]); + window._selectedAssociatedResources = Array.from(merged); + } + } catch (e) { + console.error( + "Error capturing current resource selections before search:", + e, + ); + } + // Show loading state container.innerHTML = `
@@ -22588,29 +23896,114 @@ async function serverSideResourceSearch(searchTerm) { `; if (searchTerm.trim() === "") { - // If search term is empty, reload the default prompt selector + // If search term is empty, reload the default resource selector with gateway filter try { - const response = await fetch( - `${window.ROOT_PATH}/admin/resources/partial?page=1&per_page=50&render=selector`, + const resourcesUrl = gatewayIdParam + ? `${window.ROOT_PATH}/admin/resources/partial?page=1&per_page=50&render=selector&gateway_id=${encodeURIComponent(gatewayIdParam)}` + : `${window.ROOT_PATH}/admin/resources/partial?page=1&per_page=50&render=selector`; + + console.log( + `[Resource Search] Loading default resources with URL: ${resourcesUrl}`, ); + + const response = await fetch(resourcesUrl); if (response.ok) { const html = await response.text(); - container.innerHTML = html; - // Hide no results message - if (noResultsMessage) { - noResultsMessage.style.display = "none"; + // Persist current selections to window fallback before we replace/clear the container + try { + const currentChecked = Array.from( + container.querySelectorAll( + 'input[type="checkbox"]:checked', + ), + ).map((cb) => cb.value); + if ( + !Array.isArray(window._selectedAssociatedResources) || + window._selectedAssociatedResources.length === 0 + ) { + window._selectedAssociatedResources = + currentChecked.slice(); + } else { + const merged = new Set([ + ...(window._selectedAssociatedResources || []), + ...currentChecked, + ]); + window._selectedAssociatedResources = + Array.from(merged); + } + } catch (e) { + console.error( + "Error capturing current resource selections before search:", + e, + ); } - // Initialize resource mapping if needed - initResourceSelect( - "associatedResources", - "selectedResourcesPills", - "selectedResourcesWarning", - 6, - "selectAllResourcesBtn", - "clearAllResourcesBtn", - ); + container.innerHTML = html; + + // If the container has been re-rendered server-side and our + // `data-selected-resources` attribute was lost, restore from the + // global fallback `window._selectedAssociatedResources`. + try { + // Initialize resource mapping if needed + initResourceSelect( + "associatedResources", + "selectedResourcesPills", + "selectedResourcesWarning", + 6, + "selectAllResourcesBtn", + "clearAllResourcesBtn", + ); + + const dataAttr = container.getAttribute( + "data-selected-resources", + ); + let selectedIds = null; + if (dataAttr) { + try { + selectedIds = JSON.parse(dataAttr); + } catch (e) { + console.error( + "Error parsing server data-selected-resources:", + e, + ); + } + } + + if ( + (!selectedIds || + !Array.isArray(selectedIds) || + selectedIds.length === 0) && + Array.isArray(window._selectedAssociatedResources) + ) { + selectedIds = + window._selectedAssociatedResources.slice(); + } + + if (Array.isArray(selectedIds) && selectedIds.length > 0) { + const checkboxes = container.querySelectorAll( + 'input[name="associatedResources"]', + ); + checkboxes.forEach((cb) => { + if (selectedIds.includes(cb.value)) { + cb.checked = true; + } + }); + + const firstCb = container.querySelector( + 'input[type="checkbox"]', + ); + if (firstCb) { + firstCb.dispatchEvent( + new Event("change", { bubbles: true }), + ); + } + } + } catch (e) { + console.error( + "Error restoring selections after loading default resources:", + e, + ); + } } else { container.innerHTML = '
Failed to load resources
'; @@ -22624,9 +24017,16 @@ async function serverSideResourceSearch(searchTerm) { } try { - const response = await fetch( - `${window.ROOT_PATH}/admin/resources/search?q=${encodeURIComponent(searchTerm)}&limit=100`, + // Call the search API with gateway filter + const searchUrl = gatewayIdParam + ? `${window.ROOT_PATH}/admin/resources/search?q=${encodeURIComponent(searchTerm)}&limit=100&gateway_id=${encodeURIComponent(gatewayIdParam)}` + : `${window.ROOT_PATH}/admin/resources/search?q=${encodeURIComponent(searchTerm)}&limit=100`; + + console.log( + `[Resource Search] Searching resources with URL: ${searchUrl}`, ); + + const response = await fetch(searchUrl); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } @@ -22656,6 +24056,59 @@ async function serverSideResourceSearch(searchTerm) { container.innerHTML = searchResultsHtml; + // Before initializing, ensure any persisted selections are merged into the container + try { + const existingAttr = container.getAttribute( + "data-selected-resources", + ); + let existingIds = null; + if (existingAttr) { + try { + existingIds = JSON.parse(existingAttr); + } catch (e) { + console.error( + "Error parsing existing data-selected-resources after search insert:", + e, + ); + } + } + + if ( + (!existingIds || + !Array.isArray(existingIds) || + existingIds.length === 0) && + Array.isArray(window._selectedAssociatedResources) && + window._selectedAssociatedResources.length > 0 + ) { + container.setAttribute( + "data-selected-resources", + JSON.stringify( + window._selectedAssociatedResources.slice(), + ), + ); + } else if ( + Array.isArray(existingIds) && + Array.isArray(window._selectedAssociatedResources) && + window._selectedAssociatedResources.length > 0 + ) { + const merged = new Set([ + ...(existingIds || []), + ...window._selectedAssociatedResources, + ]); + container.setAttribute( + "data-selected-resources", + JSON.stringify(Array.from(merged)), + ); + } + } catch (e) { + console.error( + "Error restoring data-selected-resources attribute after inserting search results:", + e, + ); + } + + container.innerHTML = searchResultsHtml; + // Initialize Resource select mapping initResourceSelect( "associatedResources", @@ -22701,6 +24154,43 @@ async function serverSideEditToolSearch(searchTerm) { return; } + // Get selected gateway IDs to maintain filtering + const selectedGatewayIds = getSelectedGatewayIds + ? getSelectedGatewayIds() + : []; + const gatewayIdParam = + selectedGatewayIds.length > 0 ? selectedGatewayIds.join(",") : ""; + + console.log( + `[Edit Tool Search] Searching with gateway filter: ${gatewayIdParam || "none (showing all)"}`, + ); + + // Persist current selections before we replace/clear the container + let serverToolsData = null; + let currentCheckedTools = []; + try { + // Preserve the data-server-tools attribute + const dataAttr = container.getAttribute("data-server-tools"); + if (dataAttr) { + serverToolsData = dataAttr; + } + + // Also capture currently checked items (important for search results) + currentCheckedTools = Array.from( + container.querySelectorAll('input[type="checkbox"]:checked'), + ).map((cb) => cb.value); + + console.log( + `[Edit Tool Search] Persisted ${currentCheckedTools.length} checked tools before search:`, + currentCheckedTools, + ); + } catch (e) { + console.error( + "Error preserving selections before edit tool search:", + e, + ); + } + // Show loading state container.innerHTML = `
@@ -22713,15 +24203,30 @@ async function serverSideEditToolSearch(searchTerm) { `; if (searchTerm.trim() === "") { - // If search term is empty, reload the default tool selector partial + // If search term is empty, reload the default tool selector partial with gateway filter try { - const response = await fetch( - `${window.ROOT_PATH}/admin/tools/partial?page=1&per_page=50&render=selector`, + const toolsUrl = gatewayIdParam + ? `${window.ROOT_PATH}/admin/tools/partial?page=1&per_page=50&render=selector&gateway_id=${encodeURIComponent(gatewayIdParam)}` + : `${window.ROOT_PATH}/admin/tools/partial?page=1&per_page=50&render=selector`; + + console.log( + `[Edit Tool Search] Loading default tools with URL: ${toolsUrl}`, ); + + const response = await fetch(toolsUrl); if (response.ok) { const html = await response.text(); + container.innerHTML = html; + // Restore the data-server-tools attribute after innerHTML replacement + if (serverToolsData) { + container.setAttribute( + "data-server-tools", + serverToolsData, + ); + } + // Hide no results message if (noResultsMessage) { noResultsMessage.style.display = "none"; @@ -22731,46 +24236,64 @@ async function serverSideEditToolSearch(searchTerm) { updateToolMapping(container); // Restore checked state for any tools already associated with the server + // PLUS any tools that were checked during the search try { const dataAttr = container.getAttribute("data-server-tools"); + const toolsToCheck = new Set(); + + // Add server-associated tools if (dataAttr) { const serverTools = JSON.parse(dataAttr); if ( Array.isArray(serverTools) && serverTools.length > 0 ) { - // Normalize serverTools to a set of strings for robust comparison - const serverToolSet = new Set( - serverTools.map((s) => String(s)), - ); - const checkboxes = container.querySelectorAll( - 'input[name="associatedTools"]', + serverTools.forEach((t) => + toolsToCheck.add(String(t)), ); - checkboxes.forEach((cb) => { - const toolId = cb.value; - const toolName = - cb.getAttribute("data-tool-name") || - (window.toolMapping && - window.toolMapping[cb.value]); - if ( - serverToolSet.has(toolId) || - (toolName && - serverToolSet.has(String(toolName))) - ) { - cb.checked = true; - } - }); + } + } - // Trigger update so pills/counts refresh - const firstCb = container.querySelector( - 'input[type="checkbox"]', - ); - if (firstCb) { - firstCb.dispatchEvent( - new Event("change", { bubbles: true }), - ); + // Add tools that were checked during search + if ( + Array.isArray(currentCheckedTools) && + currentCheckedTools.length > 0 + ) { + currentCheckedTools.forEach((t) => + toolsToCheck.add(String(t)), + ); + console.log( + `[Edit Tool Search] Restoring ${currentCheckedTools.length} tools checked during search`, + ); + } + + if (toolsToCheck.size > 0) { + const checkboxes = container.querySelectorAll( + 'input[name="associatedTools"]', + ); + checkboxes.forEach((cb) => { + const toolId = cb.value; + const toolName = + cb.getAttribute("data-tool-name") || + (window.toolMapping && + window.toolMapping[cb.value]); + if ( + toolsToCheck.has(toolId) || + (toolName && toolsToCheck.has(String(toolName))) + ) { + cb.checked = true; } + }); + + // Trigger update so pills/counts refresh + const firstCb = container.querySelector( + 'input[type="checkbox"]', + ); + if (firstCb) { + firstCb.dispatchEvent( + new Event("change", { bubbles: true }), + ); } } } catch (e) { @@ -22802,11 +24325,17 @@ async function serverSideEditToolSearch(searchTerm) { } try { - // Call the search API - const response = await fetch( - `${window.ROOT_PATH}/admin/tools/search?q=${encodeURIComponent(searchTerm)}&limit=100`, + // Call the search API with gateway filter + const searchUrl = gatewayIdParam + ? `${window.ROOT_PATH}/admin/tools/search?q=${encodeURIComponent(searchTerm)}&limit=100&gateway_id=${encodeURIComponent(gatewayIdParam)}` + : `${window.ROOT_PATH}/admin/tools/search?q=${encodeURIComponent(searchTerm)}&limit=100`; + + console.log( + `[Edit Tool Search] Searching tools with URL: ${searchUrl}`, ); + const response = await fetch(searchUrl); + if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } @@ -22939,6 +24468,26 @@ async function serverSideEditPromptsSearch(searchTerm) { return; } + // Get selected gateway IDs to maintain filtering + const selectedGatewayIds = getSelectedGatewayIds + ? getSelectedGatewayIds() + : []; + const gatewayIdParam = + selectedGatewayIds.length > 0 ? selectedGatewayIds.join(",") : ""; + + console.log( + `[Edit Prompt Search] Searching with gateway filter: ${gatewayIdParam || "none (showing all)"}`, + ); + + // Capture currently checked prompts BEFORE clearing the container + const currentlyCheckedPrompts = new Set(); + const existingCheckboxes = container.querySelectorAll( + 'input[name="associatedPrompts"]:checked', + ); + existingCheckboxes.forEach((cb) => { + currentlyCheckedPrompts.add(cb.value); + }); + // Show loading state container.innerHTML = `
@@ -22951,11 +24500,17 @@ async function serverSideEditPromptsSearch(searchTerm) { `; if (searchTerm.trim() === "") { - // If search term is empty, reload the default prompts selector partial + // If search term is empty, reload the default prompts selector partial with gateway filter try { - const response = await fetch( - `${window.ROOT_PATH}/admin/prompts/partial?page=1&per_page=50&render=selector`, + const promptsUrl = gatewayIdParam + ? `${window.ROOT_PATH}/admin/prompts/partial?page=1&per_page=50&render=selector&gateway_id=${encodeURIComponent(gatewayIdParam)}` + : `${window.ROOT_PATH}/admin/prompts/partial?page=1&per_page=50&render=selector`; + + console.log( + `[Edit Prompt Search] Loading default prompts with URL: ${promptsUrl}`, ); + + const response = await fetch(promptsUrl); if (response.ok) { const html = await response.text(); container.innerHTML = html; @@ -22968,51 +24523,52 @@ async function serverSideEditPromptsSearch(searchTerm) { // Update prompt mapping updatePromptMapping(container); - // Restore checked state for any prompts already associated with the server + // Restore checked state for prompts (both original server associations AND newly selected ones) try { + // Combine original server prompts with currently checked prompts + const allSelectedPrompts = new Set(currentlyCheckedPrompts); + const dataAttr = container.getAttribute( "data-server-prompts", ); if (dataAttr) { const serverPrompts = JSON.parse(dataAttr); - if ( - Array.isArray(serverPrompts) && - serverPrompts.length > 0 - ) { - // Normalize serverPrompts to a set of strings for robust comparison - const serverPromptSet = new Set( - serverPrompts.map((s) => String(s)), + if (Array.isArray(serverPrompts)) { + serverPrompts.forEach((p) => + allSelectedPrompts.add(String(p)), ); + } + } - const checkboxes = container.querySelectorAll( - 'input[name="associatedPrompts"]', - ); - checkboxes.forEach((cb) => { - const promptId = cb.value; - const promptName = - cb.getAttribute("data-prompt-name") || - (window.promptMapping && - window.promptMapping[cb.value]); - - // Check by id first (string), then by name as a fallback - if ( - serverPromptSet.has(promptId) || - (promptName && - serverPromptSet.has(String(promptName))) - ) { - cb.checked = true; - } - }); + if (allSelectedPrompts.size > 0) { + const checkboxes = container.querySelectorAll( + 'input[name="associatedPrompts"]', + ); + checkboxes.forEach((cb) => { + const promptId = cb.value; + const promptName = + cb.getAttribute("data-prompt-name") || + (window.promptMapping && + window.promptMapping[cb.value]); - // Trigger update so pills/counts refresh - const firstCb = container.querySelector( - 'input[type="checkbox"]', - ); - if (firstCb) { - firstCb.dispatchEvent( - new Event("change", { bubbles: true }), - ); + // Check by id first (string), then by name as a fallback + if ( + allSelectedPrompts.has(promptId) || + (promptName && + allSelectedPrompts.has(String(promptName))) + ) { + cb.checked = true; } + }); + + // Trigger update so pills/counts refresh + const firstCb = container.querySelector( + 'input[type="checkbox"]', + ); + if (firstCb) { + firstCb.dispatchEvent( + new Event("change", { bubbles: true }), + ); } } } catch (e) { @@ -23044,11 +24600,17 @@ async function serverSideEditPromptsSearch(searchTerm) { } try { - // Call the search API - const response = await fetch( - `${window.ROOT_PATH}/admin/prompts/search?q=${encodeURIComponent(searchTerm)}&limit=100`, + // Call the search API with gateway filter + const searchUrl = gatewayIdParam + ? `${window.ROOT_PATH}/admin/prompts/search?q=${encodeURIComponent(searchTerm)}&limit=100&gateway_id=${encodeURIComponent(gatewayIdParam)}` + : `${window.ROOT_PATH}/admin/prompts/search?q=${encodeURIComponent(searchTerm)}&limit=100`; + + console.log( + `[Edit Prompt Search] Searching prompts with URL: ${searchUrl}`, ); + const response = await fetch(searchUrl); + if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } @@ -23181,6 +24743,26 @@ async function serverSideEditResourcesSearch(searchTerm) { return; } + // Get selected gateway IDs to maintain filtering + const selectedGatewayIds = getSelectedGatewayIds + ? getSelectedGatewayIds() + : []; + const gatewayIdParam = + selectedGatewayIds.length > 0 ? selectedGatewayIds.join(",") : ""; + + console.log( + `[Edit Resource Search] Searching with gateway filter: ${gatewayIdParam || "none (showing all)"}`, + ); + + // Capture currently checked resources BEFORE clearing the container + const currentlyCheckedResources = new Set(); + const existingCheckboxes = container.querySelectorAll( + 'input[name="associatedResources"]:checked', + ); + existingCheckboxes.forEach((cb) => { + currentlyCheckedResources.add(cb.value); + }); + // Show loading state container.innerHTML = `
@@ -23193,11 +24775,17 @@ async function serverSideEditResourcesSearch(searchTerm) { `; if (searchTerm.trim() === "") { - // If search term is empty, reload the default resources selector partial + // If search term is empty, reload the default resources selector partial with gateway filter try { - const response = await fetch( - `${window.ROOT_PATH}/admin/resources/partial?page=1&per_page=50&render=selector`, + const resourcesUrl = gatewayIdParam + ? `${window.ROOT_PATH}/admin/resources/partial?page=1&per_page=50&render=selector&gateway_id=${encodeURIComponent(gatewayIdParam)}` + : `${window.ROOT_PATH}/admin/resources/partial?page=1&per_page=50&render=selector`; + + console.log( + `[Edit Resource Search] Loading default resources with URL: ${resourcesUrl}`, ); + + const response = await fetch(resourcesUrl); if (response.ok) { const html = await response.text(); container.innerHTML = html; @@ -23209,50 +24797,55 @@ async function serverSideEditResourcesSearch(searchTerm) { // Update resource mapping updateResourceMapping(container); - // Restore checked state for any resources already associated with the server + + // Restore checked state for resources (both original server associations AND newly selected ones) try { + // Combine original server resources with currently checked resources + const allSelectedResources = new Set( + currentlyCheckedResources, + ); + const dataAttr = container.getAttribute( "data-server-resources", ); if (dataAttr) { const serverResources = JSON.parse(dataAttr); - if ( - Array.isArray(serverResources) && - serverResources.length > 0 - ) { - // Normalize serverResources to a set of strings for robust comparison - const serverResourceSet = new Set( - serverResources.map((s) => String(s)), - ); - const checkboxes = container.querySelectorAll( - 'input[name="associatedResources"]', + if (Array.isArray(serverResources)) { + serverResources.forEach((r) => + allSelectedResources.add(String(r)), ); - checkboxes.forEach((cb) => { - const resourceId = cb.value; - const resourceName = - cb.getAttribute("data-resource-name") || - (window.resourceMapping && - window.resourceMapping[cb.value]); - if ( - serverResourceSet.has(resourceId) || - (resourceName && - serverResourceSet.has( - String(resourceName), - )) - ) { - cb.checked = true; - } - }); + } + } - // Trigger update so pills/counts refresh - const firstCb = container.querySelector( - 'input[type="checkbox"]', - ); - if (firstCb) { - firstCb.dispatchEvent( - new Event("change", { bubbles: true }), - ); + if (allSelectedResources.size > 0) { + const checkboxes = container.querySelectorAll( + 'input[name="associatedResources"]', + ); + checkboxes.forEach((cb) => { + const resourceId = cb.value; + const resourceName = + cb.getAttribute("data-resource-name") || + (window.resourceMapping && + window.resourceMapping[cb.value]); + if ( + allSelectedResources.has(resourceId) || + (resourceName && + allSelectedResources.has( + String(resourceName), + )) + ) { + cb.checked = true; } + }); + + // Trigger update so pills/counts refresh + const firstCb = container.querySelector( + 'input[type="checkbox"]', + ); + if (firstCb) { + firstCb.dispatchEvent( + new Event("change", { bubbles: true }), + ); } } } catch (e) { @@ -23284,11 +24877,17 @@ async function serverSideEditResourcesSearch(searchTerm) { } try { - // Call the search API - const response = await fetch( - `${window.ROOT_PATH}/admin/resources/search?q=${encodeURIComponent(searchTerm)}&limit=100`, + // Call the search API with gateway filter + const searchUrl = gatewayIdParam + ? `${window.ROOT_PATH}/admin/resources/search?q=${encodeURIComponent(searchTerm)}&limit=100&gateway_id=${encodeURIComponent(gatewayIdParam)}` + : `${window.ROOT_PATH}/admin/resources/search?q=${encodeURIComponent(searchTerm)}&limit=100`; + + console.log( + `[Edit Resource Search] Searching resources with URL: ${searchUrl}`, ); + const response = await fetch(searchUrl); + if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } diff --git a/mcpgateway/templates/admin.html b/mcpgateway/templates/admin.html index b54474f16..1d5fdb478 100644 --- a/mcpgateway/templates/admin.html +++ b/mcpgateway/templates/admin.html @@ -2208,7 +2208,7 @@

data-gateway-null="true" class="mcpserver-checkbox form-checkbox h-5 w-5 text-indigo-600 dark:bg-gray-800 dark:border-gray-600" /> - REST + REST/A2A

data-gateway-null="true" class="mcpserver-checkbox form-checkbox h-5 w-5 text-indigo-600 dark:bg-gray-800 dark:border-gray-600" /> - REST + REST/A2A