Added environment manager and updated UI
This commit is contained in:
@@ -1,65 +1,65 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<AssemblyName>EonaCat.DoxaApi</AssemblyName>
|
<AssemblyName>EonaCat.DoxaApi</AssemblyName>
|
||||||
<RootNamespace>EonaCat.DoxaApi</RootNamespace>
|
<RootNamespace>EonaCat.DoxaApi</RootNamespace>
|
||||||
|
|
||||||
<!-- NuGet package metadata -->
|
<!-- NuGet package metadata -->
|
||||||
<PackageId>EonaCat.DoxaApi</PackageId>
|
<PackageId>EonaCat.DoxaApi</PackageId>
|
||||||
<Version>0.0.7</Version>
|
<Version>0.0.8</Version>
|
||||||
<Authors>EonaCat (Jeroen Saey)</Authors>
|
<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>
|
<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>
|
<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>
|
<RepositoryUrl>https://git.saey.me/EonaCat/EonaCat.DoxaApi</RepositoryUrl>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
|
||||||
<IsPackable>true</IsPackable>
|
<IsPackable>true</IsPackable>
|
||||||
<EnableDefaultContentItems>false</EnableDefaultContentItems>
|
<EnableDefaultContentItems>false</EnableDefaultContentItems>
|
||||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||||
<Title>EonaCat.DoxaApi</Title>
|
<Title>EonaCat.DoxaApi</Title>
|
||||||
<Company>EonaCat</Company>
|
<Company>EonaCat</Company>
|
||||||
<Product>EonaCat.DoxaApi</Product>
|
<Product>EonaCat.DoxaApi</Product>
|
||||||
<Copyright>EonaCat (Jeroen Saey)</Copyright>
|
<Copyright>EonaCat (Jeroen Saey)</Copyright>
|
||||||
<PackageProjectUrl>https://git.saey.me/EonaCat/EonaCat.DoxaApi</PackageProjectUrl>
|
<PackageProjectUrl>https://git.saey.me/EonaCat/EonaCat.DoxaApi</PackageProjectUrl>
|
||||||
<PackageIcon>icon.png</PackageIcon>
|
<PackageIcon>icon.png</PackageIcon>
|
||||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="UI\Assets\**\*" />
|
<EmbeddedResource Include="UI\Assets\**\*" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Remove="images\**" />
|
<Compile Remove="images\**" />
|
||||||
<EmbeddedResource Remove="images\**" />
|
<EmbeddedResource Remove="images\**" />
|
||||||
<None Remove="images\**" />
|
<None Remove="images\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="..\icon.png">
|
<None Include="..\icon.png">
|
||||||
<Pack>True</Pack>
|
<Pack>True</Pack>
|
||||||
<PackagePath>\</PackagePath>
|
<PackagePath>\</PackagePath>
|
||||||
</None>
|
</None>
|
||||||
<None Include="..\LICENSE">
|
<None Include="..\LICENSE">
|
||||||
<Pack>True</Pack>
|
<Pack>True</Pack>
|
||||||
<PackagePath>\</PackagePath>
|
<PackagePath>\</PackagePath>
|
||||||
</None>
|
</None>
|
||||||
<None Include="..\README.md">
|
<None Include="..\README.md">
|
||||||
<Pack>True</Pack>
|
<Pack>True</Pack>
|
||||||
<PackagePath>\</PackagePath>
|
<PackagePath>\</PackagePath>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="EonaCat.Json" Version="2.2.3" />
|
<PackageReference Include="EonaCat.Json" Version="2.2.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</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 =>
|
app.Map(prefix, uiApp =>
|
||||||
{
|
{
|
||||||
uiApp.Run(async context =>
|
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;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3322,3 +3322,307 @@ code {
|
|||||||
flex: 1;
|
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";
|
"use strict";
|
||||||
|
|
||||||
const SPEC_URL = window.__DOXA_API_SPEC_URL__ || "doxaApi.json";
|
const SPEC_URL = window.__DOXA_API_SPEC_URL__ || "doxaApi.json";
|
||||||
@@ -453,9 +706,63 @@
|
|||||||
function renderTry(group, endpoint) {
|
function renderTry(group, endpoint) {
|
||||||
const methodColorVar = `var(--m-${endpoint.method.toLowerCase()}, var(--m-default))`;
|
const methodColorVar = `var(--m-${endpoint.method.toLowerCase()}, var(--m-default))`;
|
||||||
let html = `<div class="fade-in" style="--method-color:${methodColorVar}">`;
|
let html = `<div class="fade-in" style="--method-color:${methodColorVar}">`;
|
||||||
html += `<div class="try-header">
|
const currentEnv = window.DoxaEnvironmentManager.active();
|
||||||
<span class="try-title"><span class="live-dot"></span>Try it</span>
|
const currentBaseUrl = window.DoxaEnvironmentManager.currentUrl();
|
||||||
</div>`;
|
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">
|
html += `<div class="try-tabs">
|
||||||
<button class="try-tab ${activeTryTab === "body" ? "active" : ""}" data-try-tab="body">Request</button>
|
<button class="try-tab ${activeTryTab === "body" ? "active" : ""}" data-try-tab="body">Request</button>
|
||||||
@@ -573,6 +880,15 @@
|
|||||||
|
|
||||||
bindCodeTabEvents(endpoint);
|
bindCodeTabEvents(endpoint);
|
||||||
bindHistoryTabEvents(endpoint);
|
bindHistoryTabEvents(endpoint);
|
||||||
|
|
||||||
|
const changeEnvironmentBtn =
|
||||||
|
document.getElementById('changeEnvironmentBtn');
|
||||||
|
|
||||||
|
if (changeEnvironmentBtn) {
|
||||||
|
changeEnvironmentBtn.addEventListener('click', () => {
|
||||||
|
window.DoxaEnvironmentManager.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCodeTab(endpoint) {
|
function renderCodeTab(endpoint) {
|
||||||
@@ -875,6 +1191,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMockUrl() {
|
||||||
|
return new URL(window.location.href + "/mock");
|
||||||
|
}
|
||||||
|
|
||||||
async function sendTryRequest(endpoint) {
|
async function sendTryRequest(endpoint) {
|
||||||
const btn = document.getElementById("sendBtn");
|
const btn = document.getElementById("sendBtn");
|
||||||
const label = document.getElementById("sendBtnLabel");
|
const label = document.getElementById("sendBtnLabel");
|
||||||
@@ -888,8 +1208,8 @@
|
|||||||
|
|
||||||
const isMock = mockMode && mockServerAvailable();
|
const isMock = mockMode && mockServerAvailable();
|
||||||
const url = isMock
|
const url = isMock
|
||||||
? new URL("mock", window.location.href)
|
? getMockUrl()
|
||||||
: new URL(path, window.location.origin);
|
: new URL(path, (typeof window.getConfiguredBaseUrl === 'function') ? window.getConfiguredBaseUrl() : window.location.origin);
|
||||||
|
|
||||||
if (isMock) {
|
if (isMock) {
|
||||||
url.searchParams.set("operationId", endpoint.operationId);
|
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 diffFile = document.getElementById("diffFile");
|
||||||
const compareBtn = document.getElementById("compareBtn");
|
const compareBtn = document.getElementById("compareBtn");
|
||||||
compareBtn?.addEventListener("click", () => diffFile?.click());
|
compareBtn?.addEventListener("click", () => diffFile?.click());
|
||||||
@@ -1563,7 +1923,7 @@
|
|||||||
el.search.addEventListener("input", () => renderNav(el.search.value));
|
el.search.addEventListener("input", () => renderNav(el.search.value));
|
||||||
|
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (e.key === "/" && document.activeElement !== el.search) {
|
if (e.ctrlKey && e.key === "/" && document.activeElement !== el.search) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
el.search.focus();
|
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);
|
const code = CodeGenerator.generate(lang, request);
|
||||||
document.getElementById('codeBlock').textContent = code;
|
document.getElementById('codeBlock').textContent = code;
|
||||||
}
|
}
|
||||||
@@ -1951,7 +2311,7 @@
|
|||||||
*/
|
*/
|
||||||
function initializeKeyboardShortcuts() {
|
function initializeKeyboardShortcuts() {
|
||||||
const shortcuts = {
|
const shortcuts = {
|
||||||
'/': () => document.getElementById('searchInput')?.focus(),
|
'Key/': (e) => e.ctrlKey && document.getElementById('searchInput')?.focus(),
|
||||||
'KeyS': (e) => e.ctrlKey && saveCurrentRequest(),
|
'KeyS': (e) => e.ctrlKey && saveCurrentRequest(),
|
||||||
'KeyK': (e) => e.ctrlKey && toggleCommandPalette(),
|
'KeyK': (e) => e.ctrlKey && toggleCommandPalette(),
|
||||||
'Tab': (e) => handleTabNavigation(e),
|
'Tab': (e) => handleTabNavigation(e),
|
||||||
@@ -1985,7 +2345,7 @@
|
|||||||
if (searchInput && !searchInput.parentElement.querySelector('.kbd')) {
|
if (searchInput && !searchInput.parentElement.querySelector('.kbd')) {
|
||||||
const kbd = document.createElement('kbd');
|
const kbd = document.createElement('kbd');
|
||||||
kbd.className = 'kbd search-kbd';
|
kbd.className = 'kbd search-kbd';
|
||||||
kbd.textContent = '/';
|
kbd.textContent = 'Ctrl + /';
|
||||||
searchInput.parentElement.appendChild(kbd);
|
searchInput.parentElement.appendChild(kbd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2121,7 +2481,7 @@
|
|||||||
* Improved error logging
|
* Improved error logging
|
||||||
*/
|
*/
|
||||||
const originalConsoleError = console.error;
|
const originalConsoleError = console.error;
|
||||||
console.error = function(...args) {
|
console.error = function (...args) {
|
||||||
originalConsoleError.apply(console, args);
|
originalConsoleError.apply(console, args);
|
||||||
const message = args[0]?.toString?.() || String(args[0]);
|
const message = args[0]?.toString?.() || String(args[0]);
|
||||||
if (message && !message.includes('HTTP')) {
|
if (message && !message.includes('HTTP')) {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const CodeGenerator = (() => {
|
|||||||
curl: (req) => generateCurl(req),
|
curl: (req) => generateCurl(req),
|
||||||
fetch: (req) => generateFetch(req),
|
fetch: (req) => generateFetch(req),
|
||||||
axios: (req) => generateAxios(req),
|
axios: (req) => generateAxios(req),
|
||||||
httpclient: (req) => generateHttpClient(req),
|
httpclient: (req) => generateCSharp(req),
|
||||||
httppy: (req) => generatePythonHttp(req),
|
httppy: (req) => generatePythonHttp(req),
|
||||||
requests: (req) => generatePythonRequests(req),
|
requests: (req) => generatePythonRequests(req),
|
||||||
httpgo: (req) => generateGoHttp(req),
|
httpgo: (req) => generateGoHttp(req),
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
<div class="search-wrap">
|
<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>
|
<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" />
|
<input id="searchInput" type="text" placeholder="Search endpoints, methods, paths…" autocomplete="off" spellcheck="false" />
|
||||||
<kbd class="search-kbd">/</kbd>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="topbar-right">
|
<div class="topbar-right">
|
||||||
@@ -41,6 +40,28 @@
|
|||||||
<span>Auth</span>
|
<span>Auth</span>
|
||||||
<span class="auth-dot" id="authBadgeDot" style="display:none"></span>
|
<span class="auth-dot" id="authBadgeDot" style="display:none"></span>
|
||||||
</button>
|
</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">
|
<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>
|
<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>
|
<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