From 29a5699618abc1126e1e462d422b8b31d4769b61 Mon Sep 17 00:00:00 2001 From: Manav Gupta Date: Tue, 10 Jun 2025 10:10:30 -0400 Subject: [PATCH 1/3] Add a new tab for version and environment info --- mcpgateway/static/admin.js | 119 +++++++++++++----- mcpgateway/templates/admin.html | 11 +- .../templates/version_info_partial.html | 37 ++++++ mcpgateway/version.py | 11 +- tests/unit/mcpgateway/test_main.py | 24 ++++ 5 files changed, 168 insertions(+), 34 deletions(-) create mode 100644 mcpgateway/templates/version_info_partial.html diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index 3cde6b1..9d1d7fe 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -25,6 +25,63 @@ document.addEventListener("DOMContentLoaded", function () { document.getElementById("tab-metrics").addEventListener("click", () => { showTab("metrics"); }); + document.getElementById("tab-version-info").addEventListener("click", () => { + showTab("version-info"); + }); + + // Preload version info on page load (optional) + document.addEventListener("DOMContentLoaded", () => { + const panel = document.getElementById("version-info-panel"); + if (panel && panel.innerHTML.trim() === "") { + const url = `${window.ROOT_PATH}/version?partial=true`; + fetch(url) + .then((response) => { + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response.text(); + }) + .then((html) => { + panel.innerHTML = html; + // Show the version-info panel and activate the tab on page load if hash is #version-info + if (window.location.hash === "#version-info") { + showTab("version-info"); + } + }) + .cconsole.error("Failed to preload version info:", error); + atch((error) => {; + }); + } + }) + + // HTMX event listeners for debugging + document.body.addEventListener("htmx:beforeRequest", (event) => { + if (event.detail.elt.id === "tab-version-info") { + console.log("HTMX: Sending request for version info partial"); + } + ); + + document.body.addEventListener("htmx:afterSwap", (event) => { + if (event.detail.target.id === "version-info-panel") { + console.log("HTMX: Content swapped into version-info-panel" console.error("Failed to preload version info:", error); + panel.innerHTML = "

Failed to load version info.

"; + }); + } + }); + + // HTMX event listeners for debugging + document.body.addEventListener("htmx:beforeRequest", (event) => { + if (event.detail.elt.id === "tab-version-info") { + console.log("HTMX: Sending request for version info partial"); + } + }); + + document.body.addEventListener("htmx:afterSwap", (event) => { + if (event.detail.target.id === "version-info-panel") { + console.log("HTMX: Content swapped into version-info-panel"); + } + }); + // Authentication toggle document.getElementById("auth-type").addEventListener("change", function () { const basicFields = document.getElementById("auth-basic-fields"); @@ -88,7 +145,8 @@ document.addEventListener("DOMContentLoaded", function () { } else { basicFields.style.display = "none"; bearerFields.style.display = "none"; - headersFields.style.display = "none"; + heconsole.log(response); + adersFields.style.display = "none"; } }); @@ -96,7 +154,9 @@ document.addEventListener("DOMContentLoaded", function () { .getElementById("add-gateway-form") .addEventListener("submit", (e) => { e.preventDefault(); - const form = e.target; + const formerror = e.t + console.error("Error:", error); + arget; const formData = new FormData(form); fetch(`${window.ROOT_PATH}/admin/gateways`, { method: "POST", @@ -110,6 +170,7 @@ document.addEventListener("DOMContentLoaded", function () { status.classList.add("error-status"); } else { location.reload(); + console.log(response); } }) .catch((error) => { @@ -118,7 +179,9 @@ document.addEventListener("DOMContentLoaded", function () { }); document - .getElementById("add-resource-form") + .getElementBerroryId("a + console.error("Error:", error); + dd-resource-form") .addEventListener("submit", (e) => { e.preventDefault(); const form = e.target; @@ -261,7 +324,7 @@ document.addEventListener("DOMContentLoaded", function () { "edit-tool-request-type", ); - const requestTypeMap = { + const requeM= {-ed mtho, if valid MCP: ["SSE", "STDIO"], REST: ["GET", "POST", "PUT", "DELETE"], }; @@ -329,11 +392,33 @@ function showTab(tabName) { .classList.add("border-indigo-500", "text-indigo-600"); document .querySelector(`[href="#${tabName}"]`) - .classList.remove("border-transparent", "text-gray-500"); + .classconsole.error("Failed to load version info:", error); + List.remove("border-transparent", "text-gray-500"); if (tabName === "metrics") { loadAggregatedMetrics(); } + + if (tabName === "version-info") { + const panel = document.getElementById("version-info-panel"); + if (panel && panel.innerHTML.trim() === "") { + const url = `${window.ROOT_PATH}/version?partial=true`; + fetch(url) + .then((response) => { + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response.text(); + }) + .then((html) => { + panel.innerHTML = html; + }) + .catch((error) => { + console.error("Failed to load version info:", error); + panel.innerHTML = "

Failed to load version info.

"; + }); + } + } } // handle auth type selection @@ -407,29 +492,7 @@ function updateSchemaPreview() { } // Refresh CodeMirror every time Direct JSON Input is selected -Array.from(schemaModeRadios).forEach((radio) => { - radio.addEventListener("change", () => { - if (radio.value === "ui" && radio.checked) { - uiBuilderDiv.style.display = "block"; - jsonInputContainer.style.display = "none"; - } else if (radio.value === "json" && radio.checked) { - uiBuilderDiv.style.display = "none"; - jsonInputContainer.style.display = "block"; - updateSchemaPreview(); - } - }); -}); - -// Attach event listeners to dynamically added parameter inputs -function attachListeners(paramDiv) { - const inputs = paramDiv.querySelectorAll("input, select, textarea"); - inputs.forEach((input) => { - input.addEventListener("input", () => { - const mode = document.querySelector( - 'input[name="schema_input_mode"]:checked', - ).value; - if (mode === "json") { - updateSchemaPreview(); +Array.from(schemaModeant) updateSchemaPreview(); } }); }); diff --git a/mcpgateway/templates/admin.html b/mcpgateway/templates/admin.html index cb5fedb..083baec 100644 --- a/mcpgateway/templates/admin.html +++ b/mcpgateway/templates/admin.html @@ -114,15 +114,18 @@

- Version + Version and Environment Info + + +
diff --git a/mcpgateway/templates/version_info_partial.html b/mcpgateway/templates/version_info_partial.html new file mode 100644 index 0000000..bfc8f46 --- /dev/null +++ b/mcpgateway/templates/version_info_partial.html @@ -0,0 +1,37 @@ +
+
+
+ +
+
+
Version and Environment Info
+

App: {{ payload.app.name }}

+

Version: {{ payload.app.git_revision or "N/A" }}

+

Protocol Version: {{ payload.app.mcp_protocol_version }}

+

Runtime: {{ payload.platform.python }} + FastAPI {{ payload.platform.fastapi }}

+

Container: Docker

+ +
{{ payload | tojson(indent=2) }}
+
+
+
+ + diff --git a/mcpgateway/version.py b/mcpgateway/version.py index 4b4c361..6698c0d 100644 --- a/mcpgateway/version.py +++ b/mcpgateway/version.py @@ -364,14 +364,16 @@ def _login_html(next_url: str) -> str: async def version_endpoint( request: Request, fmt: Optional[str] = None, + partial: Optional[bool] = False, _user=Depends(require_auth), ) -> Response: """ - Serve diagnostics as JSON or HTML (if requested). + Serve diagnostics as JSON, full HTML, or partial HTML (if requested). Parameters: request (Request): The incoming HTTP request. - fmt (Optional[str]): Query param 'html' for HTML output. + fmt (Optional[str]): Query param 'html' for full HTML output. + partial (Optional[bool]): Query param to request partial HTML fragment. Returns: Response: JSONResponse or HTMLResponse with diagnostics data. @@ -390,6 +392,11 @@ async def version_endpoint( redis_version = str(exc) payload = _build_payload(redis_version, redis_ok) + if partial: + # Return partial HTML fragment for HTMX embedding + from fastapi.templating import Jinja2Templates + templates = Jinja2Templates(directory="mcpgateway/templates") + return templates.TemplateResponse("version_info_partial.html", {"request": request, "payload": payload}) wants_html = fmt == "html" or "text/html" in request.headers.get("accept", "") if wants_html: return HTMLResponse(_render_html(payload)) diff --git a/tests/unit/mcpgateway/test_main.py b/tests/unit/mcpgateway/test_main.py index 67e10bf..802bb66 100644 --- a/tests/unit/mcpgateway/test_main.py +++ b/tests/unit/mcpgateway/test_main.py @@ -54,6 +54,30 @@ def test_ready_check(self, test_client): assert response.status_code == 200 assert response.json()["status"] == "ready" + def test_version_partial_html(self, test_client, auth_headers): + """Test the /version endpoint with partial=true returns HTML fragment.""" + response = test_client.get("/version?partial=true", headers=auth_headers) + assert response.status_code == 200 + assert "text/html" in response.headers["content-type"] + content = response.text + assert "Version and Environment Info" in content + assert "App:" in content + + def test_admin_ui_contains_version_tab(self, test_client, auth_headers): + """Test the Admin UI contains the Version and Environment Info tab.""" + response = test_client.get("/admin", headers=auth_headers) + assert response.status_code == 200 + content = response.text + assert 'id="tab-version-info"' in content + assert "Version and Environment Info" in content + + def test_version_partial_htmx_load(self, test_client, auth_headers): + """Test HTMX request to /version?partial=true returns the partial HTML.""" + response = test_client.get("/version?partial=true", headers=auth_headers) + assert response.status_code == 200 + assert "Version and Environment Info" in response.text + assert " Date: Tue, 10 Jun 2025 21:19:15 +0100 Subject: [PATCH 2/3] Fix new UI version Signed-off-by: Mihai Criveti --- mcpgateway/static/admin.js | 132 ++++++++++++------ mcpgateway/templates/admin.html | 2 +- .../templates/version_info_partial.html | 117 ++++++++++++---- 3 files changed, 181 insertions(+), 70 deletions(-) diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index 9d1d7fe..f9c5e98 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -29,46 +29,44 @@ document.addEventListener("DOMContentLoaded", function () { showTab("version-info"); }); - // Preload version info on page load (optional) + /* ------------------------------------------------------------------ + * Pre-load the "Version & Environment Info" partial once per page + * ------------------------------------------------------------------ */ + /* Pre-load version-info once */ document.addEventListener("DOMContentLoaded", () => { const panel = document.getElementById("version-info-panel"); - if (panel && panel.innerHTML.trim() === "") { - const url = `${window.ROOT_PATH}/version?partial=true`; - fetch(url) - .then((response) => { - if (!response.ok) { - throw new Error("Network response was not ok"); - } - return response.text(); - }) - .then((html) => { - panel.innerHTML = html; - // Show the version-info panel and activate the tab on page load if hash is #version-info - if (window.location.hash === "#version-info") { - showTab("version-info"); - } - }) - .cconsole.error("Failed to preload version info:", error); - atch((error) => {; - }); - } - }) + if (!panel || panel.innerHTML.trim() !== "") return; // already loaded - // HTMX event listeners for debugging - document.body.addEventListener("htmx:beforeRequest", (event) => { - if (event.detail.elt.id === "tab-version-info") { - console.log("HTMX: Sending request for version info partial"); - } - ); + fetch(`${window.ROOT_PATH}/version?partial=true`) + .then((response) => { + if (!response.ok) throw new Error("Network response was not ok"); + return response.text(); + }) + .then((html) => { + panel.innerHTML = html; + + // If the page was opened at #version-info, show that tab now + if (window.location.hash === "#version-info") { + showTab("version-info"); + } + }) + .catch((error) => { + console.error("Failed to preload version info:", error); + panel.innerHTML = + "

Failed to load version info.

"; + }); + }); + /* ------------------------------------------------------------------ + * HTMX debug hooks + * ------------------------------------------------------------------ */ document.body.addEventListener("htmx:afterSwap", (event) => { if (event.detail.target.id === "version-info-panel") { - console.log("HTMX: Content swapped into version-info-panel" console.error("Failed to preload version info:", error); - panel.innerHTML = "

Failed to load version info.

"; - }); + console.log("HTMX: Content swapped into version-info-panel"); } }); + // HTMX event listeners for debugging document.body.addEventListener("htmx:beforeRequest", (event) => { if (event.detail.elt.id === "tab-version-info") { @@ -145,8 +143,7 @@ document.addEventListener("DOMContentLoaded", function () { } else { basicFields.style.display = "none"; bearerFields.style.display = "none"; - heconsole.log(response); - adersFields.style.display = "none"; + headersFields.style.display = "none"; } }); @@ -179,9 +176,7 @@ document.addEventListener("DOMContentLoaded", function () { }); document - .getElementBerroryId("a - console.error("Error:", error); - dd-resource-form") + .getElementById("add-resource-form") .addEventListener("submit", (e) => { e.preventDefault(); const form = e.target; @@ -324,11 +319,12 @@ document.addEventListener("DOMContentLoaded", function () { "edit-tool-request-type", ); - const requeM= {-ed mtho, if valid + const requestTypeMap = { MCP: ["SSE", "STDIO"], REST: ["GET", "POST", "PUT", "DELETE"], }; + // Optionally pass in a pre-selected method function updateEditToolRequestTypes(selectedMethod = null) { const selectedType = editToolTypeSelect.value; @@ -392,8 +388,7 @@ function showTab(tabName) { .classList.add("border-indigo-500", "text-indigo-600"); document .querySelector(`[href="#${tabName}"]`) - .classconsole.error("Failed to load version info:", error); - List.remove("border-transparent", "text-gray-500"); + .classList.remove("border-transparent", "text-gray-500"); if (tabName === "metrics") { loadAggregatedMetrics(); @@ -491,12 +486,22 @@ function updateSchemaPreview() { } } -// Refresh CodeMirror every time Direct JSON Input is selected -Array.from(schemaModeant) updateSchemaPreview(); - } - }); +/* --------------------------------------------------------------- + * Switch between "UI-builder" and "JSON input" modes + * ------------------------------------------------------------- */ +Array.from(schemaModeRadios).forEach((radio) => { + radio.addEventListener("change", () => { + if (radio.value === "ui" && radio.checked) { + uiBuilderDiv.style.display = "block"; + jsonInputContainer.style.display = "none"; + } else if (radio.value === "json" && radio.checked) { + uiBuilderDiv.style.display = "none"; + jsonInputContainer.style.display = "block"; + updateSchemaPreview(); // keep preview in sync + } }); -} +}); // closes addEventListener callback, forEach callback, and forEach call + // On form submission, update CodeMirror with UI builder schema if needed // document.getElementById('add-tool-form').addEventListener('submit', (e) => { @@ -1694,6 +1699,45 @@ async function runToolTest() { }); } +/* --------------------------------------------------------------- + * Utility: copy a JSON string (or any text) to the system clipboard + * ------------------------------------------------------------- */ +function copyJsonToClipboard(sourceId) { + // 1. Get the element that holds the JSON (can be a
, ,