Added environment manager and updated UI
This commit is contained in:
@@ -1,65 +1,65 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AssemblyName>EonaCat.DoxaApi</AssemblyName>
|
||||
<RootNamespace>EonaCat.DoxaApi</RootNamespace>
|
||||
|
||||
<!-- NuGet package metadata -->
|
||||
<PackageId>EonaCat.DoxaApi</PackageId>
|
||||
<Version>0.0.7</Version>
|
||||
<Authors>EonaCat (Jeroen Saey)</Authors>
|
||||
<Description>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.</Description>
|
||||
<PackageTags>openapi;swagger;documentation;api;aspnetcore;doxa;docs;mock-server;oauth2;codegen;Jeroen;Saey;EonaCat;Scalar;Redoc;Postman;EchoAPI;</PackageTags>
|
||||
<RepositoryUrl>https://git.saey.me/EonaCat/EonaCat.DoxaApi</RepositoryUrl>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
|
||||
<IsPackable>true</IsPackable>
|
||||
<EnableDefaultContentItems>false</EnableDefaultContentItems>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
<Title>EonaCat.DoxaApi</Title>
|
||||
<Company>EonaCat</Company>
|
||||
<Product>EonaCat.DoxaApi</Product>
|
||||
<Copyright>EonaCat (Jeroen Saey)</Copyright>
|
||||
<PackageProjectUrl>https://git.saey.me/EonaCat/EonaCat.DoxaApi</PackageProjectUrl>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="UI\Assets\**\*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="images\**" />
|
||||
<EmbeddedResource Remove="images\**" />
|
||||
<None Remove="images\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\icon.png">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
<None Include="..\LICENSE">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
<None Include="..\README.md">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AssemblyName>EonaCat.DoxaApi</AssemblyName>
|
||||
<RootNamespace>EonaCat.DoxaApi</RootNamespace>
|
||||
|
||||
<!-- NuGet package metadata -->
|
||||
<PackageId>EonaCat.DoxaApi</PackageId>
|
||||
<Version>0.0.8</Version>
|
||||
<Authors>EonaCat (Jeroen Saey)</Authors>
|
||||
<Description>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.</Description>
|
||||
<PackageTags>openapi;swagger;documentation;api;aspnetcore;doxa;docs;mock-server;oauth2;codegen;Jeroen;Saey;EonaCat;Scalar;Redoc;Postman;EchoAPI;</PackageTags>
|
||||
<RepositoryUrl>https://git.saey.me/EonaCat/EonaCat.DoxaApi</RepositoryUrl>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
|
||||
<IsPackable>true</IsPackable>
|
||||
<EnableDefaultContentItems>false</EnableDefaultContentItems>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
<Title>EonaCat.DoxaApi</Title>
|
||||
<Company>EonaCat</Company>
|
||||
<Product>EonaCat.DoxaApi</Product>
|
||||
<Copyright>EonaCat (Jeroen Saey)</Copyright>
|
||||
<PackageProjectUrl>https://git.saey.me/EonaCat/EonaCat.DoxaApi</PackageProjectUrl>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="UI\Assets\**\*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="images\**" />
|
||||
<EmbeddedResource Remove="images\**" />
|
||||
<None Remove="images\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\icon.png">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
<None Include="..\LICENSE">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
<None Include="..\README.md">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EonaCat.Json" Version="2.2.3" />
|
||||
</ItemGroup>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+371
-11
@@ -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, '&')
|
||||
.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 = `<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')) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
<div class="search-wrap">
|
||||
<svg viewBox="0 0 20 20" fill="none" class="search-ico"><circle cx="9" cy="9" r="6" stroke="currentColor" stroke-width="1.8" /><path d="M17 17L13.5 13.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" /></svg>
|
||||
<input id="searchInput" type="text" placeholder="Search endpoints, methods, paths…" autocomplete="off" spellcheck="false" />
|
||||
<kbd class="search-kbd">/</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topbar-right">
|
||||
@@ -41,6 +40,28 @@
|
||||
<span>Auth</span>
|
||||
<span class="auth-dot" id="authBadgeDot" style="display:none"></span>
|
||||
</button>
|
||||
<button id="envBtn" class="tb-btn" title="Environments">
|
||||
<svg viewBox="0 0 20 20" fill="none">
|
||||
<rect x="2.5" y="3" width="5" height="4" rx="1"
|
||||
stroke="currentColor" stroke-width="1.8" />
|
||||
<rect x="12.5" y="3" width="5" height="4" rx="1"
|
||||
stroke="currentColor" stroke-width="1.8" />
|
||||
<rect x="7.5" y="13" width="5" height="4" rx="1"
|
||||
stroke="currentColor" stroke-width="1.8" />
|
||||
|
||||
<path d="M5 7v2.5h10V7"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
|
||||
<path d="M10 9.5v3.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round" />
|
||||
</svg>
|
||||
<span>Environments</span>
|
||||
</button>
|
||||
<button id="consoleBtn" class="tb-btn" title="Console">
|
||||
<svg viewBox="0 0 20 20" fill="none"><rect x="2" y="3" width="16" height="14" rx="2" stroke="currentColor" stroke-width="1.8" /><path d="M6 8l4 4-4 4M12 16h4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
<span>Console</span>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 197 KiB |
Reference in New Issue
Block a user