Added environment manager and updated UI

This commit is contained in:
2026-06-24 19:23:50 +02:00
parent 594834908c
commit 0614ae82f5
7 changed files with 783 additions and 98 deletions
+371 -11
View File
@@ -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 => `
<div class="env-card ${env.name === activeEnv ? 'active' : ''}"
data-env="${escapeHtml(env.name)}">
<div class="env-card-left">
<input
type="radio"
name="environment"
${env.name === activeEnv ? 'checked' : ''}
>
<div>
<div class="env-name">
${escapeHtml(env.name)}
${env.name === activeEnv
? '<span class="env-badge">ACTIVE</span>'
: ''}
</div>
<div class="env-url">
${escapeHtml(env.url)}
</div>
</div>
</div>
<div class="env-actions">
<button class="icon-btn danger delete-env"
data-env="${env.name}">
🗑
</button>
</div>
</div>
`).join('');
document.body.insertAdjacentHTML('beforeend', `
<div id="envManagerModal" class="env-overlay">
<div class="env-modal">
<div class="env-modal-header">
<div>
<h2>Environment Manager</h2>
<div class="env-subtitle">
Manage API environments and switch between them
</div>
</div>
<button class="env-close-btn"
id="closeEnvManager">
</button>
</div>
<div class="env-modal-body">
<div class="env-section-title">
Environments
</div>
<div class="env-list">
${envCards}
</div>
<div class="env-divider"></div>
<div class="env-section-title">
Add Environment
</div>
<div class="env-form">
<div class="field">
<label>Name</label>
<input id="newEnvName"
type="text"
placeholder="Development">
</div>
<div class="field">
<label>Base URL</label>
<input id="newEnvUrl"
type="url"
placeholder="https://api.example.com">
</div>
<button id="addEnvBtn"
class="tb-btn tb-btn-accent">
+ Add Environment
</button>
</div>
</div>
<div class="env-modal-footer">
<button id="cancelEnvManager"
class="tb-btn">
Close
</button>
</div>
</div>
</div>
`);
}
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
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 = `<div class="fade-in" style="--method-color:${methodColorVar}">`;
html += `<div class="try-header">
<span class="try-title"><span class="live-dot"></span>Try it</span>
</div>`;
const currentEnv = window.DoxaEnvironmentManager.active();
const currentBaseUrl = window.DoxaEnvironmentManager.currentUrl();
const fullUrl =
currentBaseUrl.replace(/\/$/, '') +
endpoint.path;
html += `
<div class="try-header">
<div class="try-header-main">
<div class="try-title">
<span class="live-dot"></span>
Try it
</div>
<div class="try-subtitle">
Execute requests against the selected environment
</div>
</div>
</div>
<div class="try-env-bar">
<div class="try-env-info">
<div class="try-env-label">
Environment
</div>
<div class="try-env-main">
<span class="try-env-badge">
${escapeHtml(currentEnv)}
</span>
<span class="try-env-url">
${escapeHtml(currentBaseUrl)}
</span>
</div>
</div>
<button
id="changeEnvironmentBtn"
class="tb-btn tb-btn-sm"
type="button">
Change
</button>
</div>
<div class="request-target">
<span class="request-method method-${endpoint.method.toLowerCase()}">
${endpoint.method}
</span>
<code class="request-url">
${escapeHtml(fullUrl)}
</code>
</div>
`;
html += `<div class="try-tabs">
<button class="try-tab ${activeTryTab === "body" ? "active" : ""}" data-try-tab="body">Request</button>
@@ -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')) {