feat: update project structure and enhance UI for Edge Proxy Test

- Add .gitignore to exclude api directory
- Remove deprecated api/index.ts file
- Create new public/index.html with improved UI and functionality
- Update package.json build script to copy index.html to dist
- Enhance request handling in index.html with custom headers and presets
- Improve response display with status, time, and size metrics
This commit is contained in:
MythEclipse
2026-05-07 20:26:51 +07:00
parent 9c05003e70
commit a6333750e4
5 changed files with 1062 additions and 167 deletions
+542
View File
@@ -0,0 +1,542 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Edge Proxy Test</title>
<style>
* { box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
padding: 2rem;
background: #0f172a;
color: #e2e8f0;
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: #38bdf8;
margin-bottom: 0.5rem;
}
.subtitle {
color: #94a3b8;
margin-bottom: 2rem;
}
.panel {
background: #1e293b;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid #334155;
}
.panel h2 {
color: #38bdf8;
font-size: 1rem;
margin: 0 0 1rem 0;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.form-row {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.form-group {
flex: 1;
min-width: 200px;
}
label {
display: block;
font-size: 0.75rem;
color: #94a3b8;
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
input, textarea, select {
width: 100%;
padding: 0.75rem;
background: #0f172a;
border: 1px solid #334155;
border-radius: 8px;
color: #e2e8f0;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.875rem;
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: #38bdf8;
box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.1);
}
textarea {
min-height: 150px;
resize: vertical;
}
.method-buttons {
display: flex;
gap: 0.5rem;
}
.method-btn {
padding: 0.5rem 1rem;
background: #334155;
border: 1px solid #475569;
border-radius: 6px;
color: #e2e8f0;
cursor: pointer;
font-weight: 600;
transition: all 0.15s;
}
.method-btn:hover {
background: #475569;
}
.method-btn.active {
background: #38bdf8;
border-color: #38bdf8;
color: #0f172a;
}
.method-btn.get.active { background: #22c55e; border-color: #22c55e; }
.method-btn.post.active { background: #f97316; border-color: #f97316; }
.method-btn.put.active { background: #3b82f6; border-color: #3b82f6; }
.method-btn.delete.active { background: #ef4444; border-color: #ef4444; }
.send-btn {
width: 100%;
padding: 1rem;
background: linear-gradient(135deg, #38bdf8, #0ea5e9);
border: none;
border-radius: 8px;
color: #0f172a;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s;
}
.send-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(56, 189, 248, 0.4);
}
.send-btn:active {
transform: translateY(0);
}
.send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.response-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
@media (max-width: 768px) {
.response-section {
grid-template-columns: 1fr;
}
}
.response-meta {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.meta-badge {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: #0f172a;
border-radius: 6px;
font-size: 0.875rem;
}
.meta-badge .label {
color: #64748b;
}
.meta-badge .value {
font-weight: 600;
}
.status-2xx { color: #22c55e; }
.status-3xx { color: #eab308; }
.status-4xx { color: #f97316; }
.status-5xx { color: #ef4444; }
pre {
background: #0f172a;
padding: 1rem;
border-radius: 8px;
overflow-x: auto;
font-size: 0.8rem;
line-height: 1.5;
margin: 0;
max-height: 400px;
overflow-y: auto;
}
.error-text {
color: #ef4444;
font-family: monospace;
white-space: pre-wrap;
}
.headers-list {
font-size: 0.75rem;
}
.headers-list div {
padding: 0.25rem 0;
border-bottom: 1px solid #334155;
}
.headers-list div:last-child {
border-bottom: none;
}
.spinner {
display: inline-block;
width: 1rem;
height: 1rem;
border: 2px solid #0f172a;
border-top-color: #38bdf8;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.quick-urls {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.quick-url {
padding: 0.25rem 0.75rem;
background: #334155;
border: none;
border-radius: 4px;
color: #e2e8f0;
font-size: 0.75rem;
cursor: pointer;
transition: background 0.15s;
}
.quick-url:hover {
background: #475569;
}
.presets {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.preset-btn {
padding: 0.25rem 0.75rem;
background: #334155;
border: 1px solid #475569;
border-radius: 4px;
color: #e2e8f0;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.15s;
}
.preset-btn:hover {
background: #475569;
}
.preset-btn.active {
background: #38bdf8;
border-color: #38bdf8;
color: #0f172a;
}
</style>
</head>
<body>
<h1>Edge Proxy Relay Test</h1>
<p class="subtitle">Test your proxy with various endpoints and configurations</p>
<div class="panel">
<h2>Target Configuration</h2>
<div class="form-row">
<div class="form-group">
<label>Target URL</label>
<input type="text" id="targetUrl" value="https://httpbin.org/anything" />
</div>
</div>
<div class="quick-urls">
<span style="color: #64748b; font-size: 0.75rem;">Quick URLs:</span>
<button class="quick-url" onclick="setUrl('https://httpbin.org/anything')">httpbin /anything</button>
<button class="quick-url" onclick="setUrl('https://httpbin.org/get')">httpbin /get</button>
<button class="quick-url" onclick="setUrl('https://httpbin.org/post')">httpbin /post</button>
<button class="quick-url" onclick="setUrl('https://httpbin.org/headers')">httpbin /headers</button>
<button class="quick-url" onclick="setUrl('https://httpbin.org/uuid')">httpbin /uuid</button>
<button class="quick-url" onclick="setUrl('https://jsonplaceholder.typicode.com/posts/1')">JSONPlaceholder</button>
</div>
</div>
<div class="panel">
<h2>Request Method</h2>
<div class="method-buttons">
<button class="method-btn get active" data-method="GET" onclick="setMethod('GET')">GET</button>
<button class="method-btn post" data-method="POST" onclick="setMethod('POST')">POST</button>
<button class="method-btn put" data-method="PUT" onclick="setMethod('PUT')">PUT</button>
<button class="method-btn delete" data-method="DELETE" onclick="setMethod('DELETE')">DELETE</button>
<button class="method-btn" data-method="PATCH" onclick="setMethod('PATCH')">PATCH</button>
<button class="method-btn" data-method="HEAD" onclick="setMethod('HEAD')">HEAD</button>
<button class="method-btn" data-method="OPTIONS" onclick="setMethod('OPTIONS')">OPTIONS</button>
</div>
</div>
<div class="panel">
<h2>Request Body (JSON)</h2>
<div class="form-row">
<div class="form-group" style="flex: 2;">
<textarea id="requestBody" placeholder='{"key": "value"}'>{
"message": "Hello from Edge Proxy",
"timestamp": 1234567890,
"nested": {
"array": [1, 2, 3]
}
}</textarea>
</div>
<div class="form-group" style="flex: 1;">
<label>Presets</label>
<div class="presets">
<button type="button" class="preset-btn" onclick="setBody('empty')">Empty</button>
<button type="button" class="preset-btn" onclick="setBody('simple')">Simple</button>
<button type="button" class="preset-btn" onclick="setBody('form')">Form Data</button>
<button type="button" class="preset-btn" onclick="setBody('large')">Large JSON</button>
</div>
<label>Format</label>
<button type="button" class="preset-btn" onclick="formatJson()">Format JSON</button>
</div>
</div>
</div>
<div class="panel">
<h2>Custom Headers</h2>
<div class="form-row">
<div class="form-group">
<label>Header Key</label>
<input type="text" id="headerKey" placeholder="x-custom-header" />
</div>
<div class="form-group">
<label>Header Value</label>
<input type="text" id="headerValue" placeholder="custom-value" />
</div>
</div>
</div>
<button class="send-btn" id="sendBtn" onclick="sendRequest()">
Send Request
</button>
<div class="panel" style="margin-top: 2rem;">
<h2>Response</h2>
<div class="response-meta" id="responseMeta">
<div class="meta-badge">
<span class="label">Status:</span>
<span class="value" id="statusBadge">-</span>
</div>
<div class="meta-badge">
<span class="label">Time:</span>
<span class="value" id="timeBadge">-</span>
</div>
<div class="meta-badge">
<span class="label">Size:</span>
<span class="value" id="sizeBadge">-</span>
</div>
</div>
<div class="response-section">
<div>
<label style="color: #64748b; font-size: 0.75rem; margin-bottom: 0.5rem; display: block;">Response Body</label>
<pre id="responseBody">Response will appear here...</pre>
</div>
<div>
<label style="color: #64748b; font-size: 0.75rem; margin-bottom: 0.5rem; display: block;">Response Headers</label>
<pre id="responseHeaders" class="headers-list">Response headers will appear here...</pre>
</div>
</div>
</div>
<script>
let currentMethod = 'GET';
function setMethod(method) {
currentMethod = method;
document.querySelectorAll('.method-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.method-btn').forEach(btn => {
if (btn.dataset.method === method) btn.classList.add('active');
});
}
function setUrl(url) {
document.getElementById('targetUrl').value = url;
}
function setBody(type) {
const textarea = document.getElementById('requestBody');
switch (type) {
case 'empty':
textarea.value = '';
break;
case 'simple':
textarea.value = JSON.stringify({ hello: 'world' }, null, 2);
break;
case 'form':
textarea.value = JSON.stringify({
name: 'John Doe',
email: 'john@example.com',
message: 'Hello!'
}, null, 2);
break;
case 'large':
textarea.value = JSON.stringify({
users: Array.from({ length: 100 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
active: i % 2 === 0
}))
}, null, 2);
break;
}
}
function formatJson() {
const textarea = document.getElementById('requestBody');
try {
const parsed = JSON.parse(textarea.value);
textarea.value = JSON.stringify(parsed, null, 2);
} catch (e) {
alert('Invalid JSON: ' + e.message);
}
}
function getStatusClass(status) {
if (status >= 200 && status < 300) return 'status-2xx';
if (status >= 300 && status < 400) return 'status-3xx';
if (status >= 400 && status < 500) return 'status-4xx';
return 'status-5xx';
}
async function sendRequest() {
const btn = document.getElementById('sendBtn');
const targetUrl = document.getElementById('targetUrl').value;
const requestBody = document.getElementById('requestBody').value;
const headerKey = document.getElementById('headerKey').value;
const headerValue = document.getElementById('headerValue').value;
// Update UI
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Sending...';
document.getElementById('responseBody').textContent = 'Sending request...';
document.getElementById('responseHeaders').textContent = '-';
document.getElementById('statusBadge').textContent = '-';
document.getElementById('timeBadge').textContent = '-';
document.getElementById('sizeBadge').textContent = '-';
const headers = {
'x-relay-target': targetUrl,
'Content-Type': 'application/json'
};
if (headerKey && headerValue) {
headers[headerKey] = headerValue;
}
try {
const start = Date.now();
const fetchOptions = {
method: currentMethod,
headers
};
if (!['GET', 'HEAD', 'OPTIONS'].includes(currentMethod) && requestBody) {
fetchOptions.body = requestBody;
}
const res = await fetch('/api', fetchOptions);
const elapsed = Date.now() - start;
// Parse response
let responseText = '';
let responseData = null;
const contentType = res.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
responseData = await res.json();
responseText = JSON.stringify(responseData, null, 2);
} else {
responseText = await res.text();
}
// Get response headers
const headersObj = {};
res.headers.forEach((value, key) => {
headersObj[key] = value;
});
// Update UI
const statusEl = document.getElementById('statusBadge');
statusEl.textContent = `${res.status} ${res.statusText}`;
statusEl.className = `value ${getStatusClass(res.status)}`;
document.getElementById('timeBadge').textContent = `${elapsed}ms`;
document.getElementById('sizeBadge').textContent = formatBytes(new Blob([responseText]).size);
document.getElementById('responseBody').textContent = responseText;
document.getElementById('responseHeaders').textContent = JSON.stringify(headersObj, null, 2);
} catch (err) {
document.getElementById('responseBody').innerHTML = `<span class="error-text">Error: ${err.message}</span>`;
document.getElementById('statusBadge').textContent = 'Error';
document.getElementById('statusBadge').className = 'value status-5xx';
} finally {
btn.disabled = false;
btn.textContent = 'Send Request';
}
}
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
</script>
</body>
</html>