diff --git a/DoxaApi/EonaCat.DoxaApi.csproj b/DoxaApi/EonaCat.DoxaApi.csproj index 21d00db..76ca871 100644 --- a/DoxaApi/EonaCat.DoxaApi.csproj +++ b/DoxaApi/EonaCat.DoxaApi.csproj @@ -1,65 +1,65 @@ - - - - net8.0 - enable - enable - latest - EonaCat.DoxaApi - EonaCat.DoxaApi - - - EonaCat.DoxaApi - 0.0.7 - EonaCat (Jeroen Saey) - A modern, self-contained, dependency-free API documentation UI for ASP.NET Core with a built-in auth manager, mock server, server-side request console, multi-language code generation, and spec diffing. - openapi;swagger;documentation;api;aspnetcore;doxa;docs;mock-server;oauth2;codegen;Jeroen;Saey;EonaCat;Scalar;Redoc;Postman;EchoAPI; - https://git.saey.me/EonaCat/EonaCat.DoxaApi - README.md - true - - true - false - True - EonaCat.DoxaApi - EonaCat - EonaCat.DoxaApi - EonaCat (Jeroen Saey) - https://git.saey.me/EonaCat/EonaCat.DoxaApi - icon.png - LICENSE - - - - - - - - - - - - - - - - - - - True - \ - - - True - \ - - - True - \ - - - + + + + net8.0 + enable + enable + latest + EonaCat.DoxaApi + EonaCat.DoxaApi + + + EonaCat.DoxaApi + 0.0.8 + EonaCat (Jeroen Saey) + A modern, self-contained, dependency-free API documentation UI for ASP.NET Core with a built-in auth manager, mock server, server-side request console, multi-language code generation, and spec diffing. + openapi;swagger;documentation;api;aspnetcore;doxa;docs;mock-server;oauth2;codegen;Jeroen;Saey;EonaCat;Scalar;Redoc;Postman;EchoAPI; + https://git.saey.me/EonaCat/EonaCat.DoxaApi + README.md + true + + true + false + True + EonaCat.DoxaApi + EonaCat + EonaCat.DoxaApi + EonaCat (Jeroen Saey) + https://git.saey.me/EonaCat/EonaCat.DoxaApi + icon.png + LICENSE + + + + + + + + + + + + + + + + + + + True + \ + + + True + \ + + + True + \ + + + - + \ No newline at end of file diff --git a/DoxaApi/Middleware/DoxaApiMiddlewareExtensions.cs b/DoxaApi/Middleware/DoxaApiMiddlewareExtensions.cs index 4a9c8b5..0c527ca 100644 --- a/DoxaApi/Middleware/DoxaApiMiddlewareExtensions.cs +++ b/DoxaApi/Middleware/DoxaApiMiddlewareExtensions.cs @@ -166,6 +166,29 @@ namespace EonaCat.DoxaApi.Middleware }); } + // Add analytics endpoint if metrics collector is registered + if (metricsCollector != null) + { + app.Map($"{prefix}/analytics", analyticsApp => + { + analyticsApp.Run(async context => + { + if (!HttpMethods.IsGet(context.Request.Method)) + { + context.Response.StatusCode = 405; + context.Response.Headers["Allow"] = "GET"; + await context.Response.WriteAsync("Method Not Allowed - use GET"); + return; + } + + var dashboard = metricsCollector.GetDashboard(); + context.Response.ContentType = "application/json; charset=utf-8"; + var json = JsonHelper.ToJson(dashboard, _writeOptions); + await context.Response.WriteAsync(json, Encoding.UTF8); + }); + }); + } + app.Map(prefix, uiApp => { uiApp.Run(async context => @@ -203,29 +226,6 @@ namespace EonaCat.DoxaApi.Middleware }); }); - // Add analytics endpoint if metrics collector is registered - if (metricsCollector != null) - { - app.Map(prefix + "/analytics", analyticsApp => - { - analyticsApp.Run(async context => - { - if (!HttpMethods.IsGet(context.Request.Method)) - { - context.Response.StatusCode = 405; - context.Response.Headers["Allow"] = "GET"; - await context.Response.WriteAsync("Method Not Allowed - use GET"); - return; - } - - var dashboard = metricsCollector.GetDashboard(); - context.Response.ContentType = "application/json; charset=utf-8"; - var json = JsonHelper.ToJson(dashboard, _writeOptions); - await context.Response.WriteAsync(json, Encoding.UTF8); - }); - }); - } - return app; } diff --git a/DoxaApi/UI/Assets/app.css b/DoxaApi/UI/Assets/app.css index 9ed707f..1f8c303 100644 --- a/DoxaApi/UI/Assets/app.css +++ b/DoxaApi/UI/Assets/app.css @@ -3322,3 +3322,307 @@ code { flex: 1; } } + +.env-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,.65); + backdrop-filter: blur(6px); + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; + animation: fadeIn .15s ease; +} + +.env-modal { + width: min(850px, 92vw); + max-height: 90vh; + overflow: auto; + background: #1f1f23; + color: #fff; + border-radius: 18px; + border: 1px solid #35353a; + box-shadow: 0 24px 80px rgba(0,0,0,.45), 0 0 0 1px rgba(255,255,255,.03); + animation: scaleIn .18s ease; +} + +.env-modal-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 24px; + border-bottom: 1px solid #35353a; +} + + .env-modal-header h2 { + margin: 0; + font-size: 22px; + } + +.env-subtitle { + margin-top: 6px; + color: #9aa0aa; + font-size: 13px; +} + +.env-close-btn { + width: 36px; + height: 36px; + border-radius: 10px; + border: none; + cursor: pointer; + background: transparent; + color: #aaa; + font-size: 16px; +} + + .env-close-btn:hover { + background: rgba(255,255,255,.08); + color: white; + } + +.env-modal-body { + padding: 24px; +} + +.env-section-title { + font-size: 13px; + font-weight: 600; + color: #9aa0aa; + text-transform: uppercase; + letter-spacing: .08em; + margin-bottom: 12px; +} + +.env-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.env-card { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + border-radius: 14px; + border: 1px solid #35353a; + transition: .15s ease; + cursor: pointer; +} + + .env-card:hover { + border-color: #5a9cff; + transform: translateY(-1px); + } + + .env-card.active { + border-color: #5a9cff; + background: rgba(90,156,255,.12); + } + +.env-card-left { + display: flex; + gap: 14px; + align-items: center; +} + +.env-name { + display: flex; + align-items: center; + gap: 10px; + font-weight: 600; +} + +.env-url { + margin-top: 4px; + color: #9aa0aa; + font-size: 13px; +} + +.env-badge { + padding: 3px 8px; + border-radius: 999px; + font-size: 11px; + background: #5a9cff; + color: white; +} + +.env-actions { + display: flex; + gap: 8px; +} + +.icon-btn { + width: 34px; + height: 34px; + border-radius: 10px; + border: 1px solid #35353a; + background: transparent; + color: white; + cursor: pointer; +} + + .icon-btn:hover { + background: rgba(255,255,255,.08); + } + + .icon-btn.danger:hover { + background: rgba(255,80,80,.15); + } + +.try-header-main { + display: flex; + flex-direction: column; + gap: 4px; +} + +.try-subtitle { + color: var(--text-2); + font-size: 13px; +} + +.try-env-bar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 16px; + padding: 12px 14px; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--bg-2); +} + +.try-env-info { + min-width: 0; + flex: 1; +} + +.try-env-label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: .08em; + color: var(--text-2); + margin-bottom: 6px; +} + +.try-env-main { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.try-env-badge { + padding: 4px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 600; + background: rgba(59,130,246,.15); + color: #60a5fa; + border: 1px solid rgba(59,130,246,.25); +} + +.try-env-url { + font-family: var(--font-mono); + font-size: 13px; + color: var(--text-2); + overflow: hidden; + text-overflow: ellipsis; +} + +.request-target { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 16px; + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--bg-2); +} + +.request-method { + padding: 4px 8px; + border-radius: 6px; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + color: white; + background: var(--method-color); +} + +.request-url { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: var(--font-mono); + color: var(--text-1); +} + +.env-divider { + margin: 24px 0; + border-top: 1px solid #35353a; +} + +.env-form { + display: grid; + gap: 14px; +} + +.field { + display: grid; + gap: 6px; +} + + .field label { + font-size: 13px; + color: #9aa0aa; + } + + .field input { + height: 42px; + padding: 0 12px; + border-radius: 10px; + border: 1px solid #35353a; + background: #27272c; + color: white; + } + + .field input:focus { + outline: none; + border-color: #5a9cff; + } + +.env-modal-footer { + display: flex; + justify-content: flex-end; + gap: 10px; + padding: 20px 24px; + border-top: 1px solid #35353a; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(.96); + } + + to { + opacity: 1; + transform: scale(1); + } +} \ No newline at end of file diff --git a/DoxaApi/UI/Assets/app.js b/DoxaApi/UI/Assets/app.js index d5d9140..bbb279c 100644 --- a/DoxaApi/UI/Assets/app.js +++ b/DoxaApi/UI/Assets/app.js @@ -1,4 +1,257 @@ -(function () { + +window.DoxaEnvironmentManager = (function () { + const KEY = 'DoxaApi.environments'; + const ACTIVE = 'DoxaApi.activeEnvironment'; + function load() { + const stored = localStorage.getItem(KEY); + if (stored) { + try { return JSON.parse(stored); } catch (e) { /* fall through to default below */ } + } + return [{ name: 'Local', url: window.location.origin }]; + } + function save(v) { localStorage.setItem(KEY, JSON.stringify(v)); } + function active() { return localStorage.getItem(ACTIVE) || 'Local'; } + function currentUrl() { const env = load().find(x => x.name === active()); return env ? env.url : window.location.origin; } + window.getConfiguredBaseUrl = currentUrl; + + function hideEnvironmentManager() { + const modal = document.getElementById('envManagerModal'); + if (modal) modal.remove(); + } + + function show() { + const existing = document.getElementById('envManagerModal'); + if (existing) existing.remove(); + + const envs = load(); + const activeEnv = active(); + + const envCards = envs.map(env => ` +
+
+ + +
+
+ ${escapeHtml(env.name)} + ${env.name === activeEnv + ? 'ACTIVE' + : ''} +
+ +
+ ${escapeHtml(env.url)} +
+
+
+ +
+ +
+
+ `).join(''); + + document.body.insertAdjacentHTML('beforeend', ` +
+
+ +
+
+

Environment Manager

+
+ Manage API environments and switch between them +
+
+ + +
+ +
+ +
+ Environments +
+ +
+ ${envCards} +
+ +
+ +
+ Add Environment +
+ +
+
+ + +
+ +
+ + +
+ + +
+ +
+ + + +
+
+ `); + } + + function escapeHtml(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + document.addEventListener('click', e => { + + // Close button + if ( + e.target.id === 'closeEnvManager' || + e.target.id === 'cancelEnvManager' + ) { + window.DoxaEnvironmentManager.hide(); + return; + } + + // Overlay click + if ( + e.target.classList && + e.target.classList.contains('env-overlay') + ) { + window.DoxaEnvironmentManager.hide(); + return; + } + + // Select environment + const card = e.target.closest('.env-card'); + if (card && !e.target.closest('.env-actions')) { + localStorage.setItem(ACTIVE, card.dataset.env); + + window.DoxaEnvironmentManager.hide(); + window.DoxaEnvironmentManager.show(); + return; + } + + // Delete environment + const deleteBtn = e.target.closest('.delete-env'); + if (deleteBtn) { + + const envName = deleteBtn.dataset.env; + + if (envName === 'Local') { + alert('The Local environment cannot be deleted.'); + return; + } + + const envs = load().filter(x => x.name !== envName); + + window.DoxaEnvironmentManager.save(envs); + + if (active() === envName) { + localStorage.setItem(ACTIVE, 'Local'); + } + + window.DoxaEnvironmentManager.hide(); + window.DoxaEnvironmentManager.show(); + return; + } + + // Add environment + if (e.target.id === 'addEnvBtn') { + + const name = document + .getElementById('newEnvName') + .value + .trim(); + + const url = document + .getElementById('newEnvUrl') + .value + .trim(); + + if (!name || !url) { + alert('Please enter both a name and URL.'); + return; + } + + try { + new URL(url); + } catch { + alert('Please enter a valid URL.'); + return; + } + + const envs = load(); + + if (envs.some(x => x.name === name)) { + alert('An environment with that name already exists.'); + return; + } + + envs.push({ + name, + url + }); + + save(envs); + + hide(); + show(); + } + }); + + document.addEventListener('keydown', e => { + if (e.key === 'Escape') { + hide(); + } + }); + + + return { + show, + currentUrl, + active, + hide: hideEnvironmentManager + }; +})(); + + +(function () { "use strict"; const SPEC_URL = window.__DOXA_API_SPEC_URL__ || "doxaApi.json"; @@ -453,9 +706,63 @@ function renderTry(group, endpoint) { const methodColorVar = `var(--m-${endpoint.method.toLowerCase()}, var(--m-default))`; let html = `
`; - html += `
- Try it -
`; + const currentEnv = window.DoxaEnvironmentManager.active(); + const currentBaseUrl = window.DoxaEnvironmentManager.currentUrl(); + const fullUrl = + currentBaseUrl.replace(/\/$/, '') + + endpoint.path; + + html += ` +
+
+
+ + Try it +
+ +
+ Execute requests against the selected environment +
+
+
+ +
+
+ +
+ Environment +
+ +
+ + ${escapeHtml(currentEnv)} + + + + ${escapeHtml(currentBaseUrl)} + +
+ +
+ + +
+ +
+ + ${endpoint.method} + + + + ${escapeHtml(fullUrl)} + +
+`; html += `
@@ -573,6 +880,15 @@ bindCodeTabEvents(endpoint); bindHistoryTabEvents(endpoint); + + const changeEnvironmentBtn = + document.getElementById('changeEnvironmentBtn'); + + if (changeEnvironmentBtn) { + changeEnvironmentBtn.addEventListener('click', () => { + window.DoxaEnvironmentManager.show(); + }); + } } function renderCodeTab(endpoint) { @@ -875,6 +1191,10 @@ } } + function getMockUrl() { + return new URL(window.location.href + "/mock"); + } + async function sendTryRequest(endpoint) { const btn = document.getElementById("sendBtn"); const label = document.getElementById("sendBtnLabel"); @@ -888,8 +1208,8 @@ const isMock = mockMode && mockServerAvailable(); const url = isMock - ? new URL("mock", window.location.href) - : new URL(path, window.location.origin); + ? getMockUrl() + : new URL(path, (typeof window.getConfiguredBaseUrl === 'function') ? window.getConfiguredBaseUrl() : window.location.origin); if (isMock) { url.searchParams.set("operationId", endpoint.operationId); @@ -1478,6 +1798,46 @@ } } + const envBtn = document.getElementById("envBtn"); + if (envBtn) { + document.addEventListener("click", (e) => { + if (e.target.closest('#envBtn')) { + window.DoxaEnvironmentManager.show(); + return; + } + + if ( + e.target.id === 'closeEnvManager' || + e.target.id === 'cancelEnvManager' || + e.target.classList.contains('env-overlay') + ) { + window.DoxaEnvironmentManager.hide(); + return; + } + + const card = e.target.closest('.env-card'); + if (card) { + const envName = card.dataset.env; + + window.DoxaEnvironmentManager.setActive(envName); + + document + .querySelectorAll('.env-card') + .forEach(c => c.classList.remove('active')); + + card.classList.add('active'); + + document + .querySelectorAll('input[name="environment"]') + .forEach(r => r.checked = false); + + card.querySelector('input[type="radio"]').checked = true; + } + }); + } else { + envBtn.addEventListener("click", () => showEnvironmentManager()); + } + const diffFile = document.getElementById("diffFile"); const compareBtn = document.getElementById("compareBtn"); compareBtn?.addEventListener("click", () => diffFile?.click()); @@ -1563,7 +1923,7 @@ el.search.addEventListener("input", () => renderNav(el.search.value)); document.addEventListener("keydown", (e) => { - if (e.key === "/" && document.activeElement !== el.search) { + if (e.ctrlKey && e.key === "/" && document.activeElement !== el.search) { e.preventDefault(); el.search.focus(); } @@ -1878,7 +2238,7 @@ } } - const request = RequestBuilder.buildRequest(endpoint, paramState, window.location.origin); + const request = RequestBuilder.buildRequest(endpoint, paramState, (typeof window.getConfiguredBaseUrl === 'function') ? window.getConfiguredBaseUrl() : window.location.origin); const code = CodeGenerator.generate(lang, request); document.getElementById('codeBlock').textContent = code; } @@ -1951,7 +2311,7 @@ */ function initializeKeyboardShortcuts() { const shortcuts = { - '/': () => document.getElementById('searchInput')?.focus(), + 'Key/': (e) => e.ctrlKey && document.getElementById('searchInput')?.focus(), 'KeyS': (e) => e.ctrlKey && saveCurrentRequest(), 'KeyK': (e) => e.ctrlKey && toggleCommandPalette(), 'Tab': (e) => handleTabNavigation(e), @@ -1985,7 +2345,7 @@ if (searchInput && !searchInput.parentElement.querySelector('.kbd')) { const kbd = document.createElement('kbd'); kbd.className = 'kbd search-kbd'; - kbd.textContent = '/'; + kbd.textContent = 'Ctrl + /'; searchInput.parentElement.appendChild(kbd); } } @@ -2121,7 +2481,7 @@ * Improved error logging */ const originalConsoleError = console.error; - console.error = function(...args) { + console.error = function (...args) { originalConsoleError.apply(console, args); const message = args[0]?.toString?.() || String(args[0]); if (message && !message.includes('HTTP')) { diff --git a/DoxaApi/UI/Assets/code-generator.js b/DoxaApi/UI/Assets/code-generator.js index a5da9d5..0ed8e75 100644 --- a/DoxaApi/UI/Assets/code-generator.js +++ b/DoxaApi/UI/Assets/code-generator.js @@ -28,7 +28,7 @@ const CodeGenerator = (() => { curl: (req) => generateCurl(req), fetch: (req) => generateFetch(req), axios: (req) => generateAxios(req), - httpclient: (req) => generateHttpClient(req), + httpclient: (req) => generateCSharp(req), httppy: (req) => generatePythonHttp(req), requests: (req) => generatePythonRequests(req), httpgo: (req) => generateGoHttp(req), diff --git a/DoxaApi/UI/Assets/index.html b/DoxaApi/UI/Assets/index.html index ea1805a..4b466b9 100644 --- a/DoxaApi/UI/Assets/index.html +++ b/DoxaApi/UI/Assets/index.html @@ -28,7 +28,6 @@
- /
@@ -41,6 +40,28 @@ Auth +