Updated UI
This commit is contained in:
+455
-2
@@ -1610,7 +1610,7 @@
|
||||
dashboardPanel.style.display = "block";
|
||||
|
||||
try {
|
||||
const res = await fetch("analytics");
|
||||
const res = await fetch(new URL("analytics", document.baseURI));
|
||||
if (!res.ok) throw new Error("Failed to load analytics");
|
||||
|
||||
const analyticsData = await res.json();
|
||||
@@ -1619,7 +1619,7 @@
|
||||
// Refresh every 5 seconds
|
||||
if (window.dashboardInterval) clearInterval(window.dashboardInterval);
|
||||
window.dashboardInterval = setInterval(async () => {
|
||||
const res = await fetch("analytics");
|
||||
const res = await fetch(new URL("analytics", document.baseURI));
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
updateDashboard(data);
|
||||
@@ -1689,5 +1689,458 @@
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced Try Pane - Advanced Request Builder
|
||||
* Integrates RequestBuilder and CodeGenerator modules
|
||||
*/
|
||||
function renderAdvancedTryPane(group, endpoint) {
|
||||
if (!endpoint) return renderTryEmpty();
|
||||
|
||||
const paramState = new RequestBuilder.ParameterState();
|
||||
RequestBuilder.populateDefaults(endpoint, paramState);
|
||||
|
||||
let html = `
|
||||
<div class="try-wrap">
|
||||
<div class="try-header">
|
||||
<div class="try-title">${escapeHtml(endpoint.method)} ${escapeHtml(endpoint.path)}</div>
|
||||
${endpoint.deprecated ? '<span class="deprecated-badge">DEPRECATED</span>' : ''}
|
||||
</div>
|
||||
|
||||
<div class="params-editor">
|
||||
`;
|
||||
|
||||
// Path parameters
|
||||
if (endpoint.parameters?.filter(p => p.in === 'path').length > 0) {
|
||||
html += renderParamGroup('Path Parameters', endpoint.parameters.filter(p => p.in === 'path'), paramState);
|
||||
}
|
||||
|
||||
// Query parameters
|
||||
if (endpoint.parameters?.filter(p => p.in === 'query').length > 0) {
|
||||
html += renderParamGroup('Query Parameters', endpoint.parameters.filter(p => p.in === 'query'), paramState);
|
||||
}
|
||||
|
||||
// Headers
|
||||
if (endpoint.parameters?.filter(p => p.in === 'header').length > 0) {
|
||||
html += renderParamGroup('Headers', endpoint.parameters.filter(p => p.in === 'header'), paramState);
|
||||
}
|
||||
|
||||
// Request body
|
||||
if (endpoint.requestBody) {
|
||||
html += `
|
||||
<div class="param-group">
|
||||
<div class="param-group-header">
|
||||
<svg class="param-group-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M6 9l6 6 6-6"/>
|
||||
</svg>
|
||||
Request Body
|
||||
</div>
|
||||
<div class="param-items">
|
||||
<div class="param-input" style="height: 200px; font-family: var(--font-mono); padding: 10px;" contenteditable="true" data-request-body="true">
|
||||
${endpoint.requestBody.example ? JSON.stringify(endpoint.requestBody.example, null, 2) : '{}'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
// Code Generator
|
||||
html += `
|
||||
<div class="code-generator">
|
||||
<div style="font-size: 12px; font-weight: 600; margin-bottom: 8px;">Generate Code</div>
|
||||
<div class="code-lang-select" id="codeLangSelect">
|
||||
`;
|
||||
|
||||
CodeGenerator.getSupportedLanguages().forEach(lang => {
|
||||
html += `<button class="code-lang-btn" data-lang="${lang}">${lang}</button>`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</div>
|
||||
<div class="code-block" id="codeBlock">
|
||||
// Select a language above
|
||||
</div>
|
||||
<button class="code-copy-btn" id="codeCopyBtn">Copy Code</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Response preview area
|
||||
html += `
|
||||
<div class="response-preview" style="display: none;" id="responsePreview">
|
||||
<div class="response-header">
|
||||
<span class="response-status" id="responseStatus"></span>
|
||||
<span class="response-timing" id="responseTiming"></span>
|
||||
</div>
|
||||
<div class="response-tabs">
|
||||
<button class="response-tab active" data-tab="body">Body</button>
|
||||
<button class="response-tab" data-tab="headers">Headers</button>
|
||||
</div>
|
||||
<div class="response-body" id="responseBody"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
el.try.innerHTML = html;
|
||||
|
||||
// Bind code generator
|
||||
document.querySelectorAll('.code-lang-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
document.querySelectorAll('.code-lang-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
activeCodeLang = btn.dataset.lang;
|
||||
updateCodePreview(endpoint, paramState);
|
||||
});
|
||||
});
|
||||
|
||||
// Initial code preview
|
||||
updateCodePreview(endpoint, paramState);
|
||||
|
||||
// Copy code button
|
||||
document.getElementById('codeCopyBtn')?.addEventListener('click', () => {
|
||||
const code = document.getElementById('codeBlock').textContent;
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
const btn = document.getElementById('codeCopyBtn');
|
||||
btn.classList.add('copied');
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(() => {
|
||||
btn.classList.remove('copied');
|
||||
btn.textContent = 'Copy Code';
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render parameter group
|
||||
*/
|
||||
function renderParamGroup(name, parameters, paramState) {
|
||||
if (!parameters.length) return '';
|
||||
|
||||
const paramIn = parameters[0].in;
|
||||
let html = `
|
||||
<div class="param-group">
|
||||
<div class="param-group-header" data-toggle-group="${paramIn}">
|
||||
<svg class="param-group-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M6 9l6 6 6-6"/>
|
||||
</svg>
|
||||
${name}
|
||||
</div>
|
||||
<div class="param-items">
|
||||
`;
|
||||
|
||||
parameters.forEach(param => {
|
||||
html += `
|
||||
<div class="param-row ${param.required ? 'required' : ''}">
|
||||
<div class="param-name" title="${escapeHtml(param.description || param.name)}">${escapeHtml(param.name)}</div>
|
||||
<input
|
||||
type="text"
|
||||
class="param-input"
|
||||
data-param-in="${param.in}"
|
||||
data-param-name="${param.name}"
|
||||
placeholder="${param.schema?.description || ''}"
|
||||
value="${paramState[param.in]?.[param.name] || ''}"
|
||||
/>
|
||||
<div class="param-type">${param.schema?.type || 'string'}</div>
|
||||
<button class="param-remove" title="Remove parameter">×</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `</div></div>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update code preview based on current parameters
|
||||
*/
|
||||
function updateCodePreview(endpoint, paramState) {
|
||||
const currentLangBtn = document.querySelector('.code-lang-btn.active');
|
||||
const lang = currentLangBtn?.dataset.lang || 'curl';
|
||||
|
||||
// Gather current param values from UI
|
||||
document.querySelectorAll('.param-input').forEach(input => {
|
||||
const paramIn = input.dataset.paramIn;
|
||||
const paramName = input.dataset.paramName;
|
||||
if (paramIn && paramName) {
|
||||
paramState[paramIn][paramName] = input.value;
|
||||
}
|
||||
});
|
||||
|
||||
// Get request body if present
|
||||
const bodyInput = document.querySelector('[data-request-body="true"]');
|
||||
if (bodyInput) {
|
||||
try {
|
||||
paramState.body = JSON.parse(bodyInput.textContent);
|
||||
} catch (e) {
|
||||
paramState.body = bodyInput.textContent;
|
||||
}
|
||||
}
|
||||
|
||||
const request = RequestBuilder.buildRequest(endpoint, paramState, window.location.origin);
|
||||
const code = CodeGenerator.generate(lang, request);
|
||||
document.getElementById('codeBlock').textContent = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Integration point for workspace manager
|
||||
*/
|
||||
function initializeWorkspaceManager() {
|
||||
// Load initial workspaces
|
||||
const workspaces = WorkspaceManager.getWorkspaces();
|
||||
if (workspaces.length === 0) {
|
||||
const defaultWs = new WorkspaceManager.Workspace('My Workspace');
|
||||
const defaultCol = new WorkspaceManager.Collection('API Collection');
|
||||
defaultWs.collections.push(defaultCol);
|
||||
WorkspaceManager.saveWorkspace(defaultWs);
|
||||
}
|
||||
|
||||
// Setup environment selector if environments panel exists
|
||||
const envList = document.querySelector('.env-list');
|
||||
if (envList) {
|
||||
const environments = WorkspaceManager.getEnvironments();
|
||||
if (environments.length === 0) {
|
||||
const defaultEnv = new WorkspaceManager.Environment('Default Environment');
|
||||
WorkspaceManager.saveEnvironment(defaultEnv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save current request to collection
|
||||
*/
|
||||
function saveCurrentRequest() {
|
||||
if (!activeEndpoint || !spec) return;
|
||||
|
||||
const name = prompt('Request name:', `${activeEndpoint.method} ${activeEndpoint.path}`);
|
||||
if (!name) return;
|
||||
|
||||
const workspace = WorkspaceManager.getCurrentWorkspace();
|
||||
const collection = workspace.collections[0];
|
||||
if (!collection) return;
|
||||
|
||||
const saved = new WorkspaceManager.SavedRequest(
|
||||
name,
|
||||
activeEndpoint.method,
|
||||
activeEndpoint.path
|
||||
);
|
||||
saved.description = activeEndpoint.description || '';
|
||||
|
||||
// Gather current parameters
|
||||
document.querySelectorAll('.param-input').forEach(input => {
|
||||
const paramIn = input.dataset.paramIn;
|
||||
const paramName = input.dataset.paramName;
|
||||
if (paramIn && paramName && input.value) {
|
||||
saved.params[paramIn][paramName] = input.value;
|
||||
}
|
||||
});
|
||||
|
||||
// Save body
|
||||
const bodyInput = document.querySelector('[data-request-body="true"]');
|
||||
if (bodyInput) {
|
||||
saved.body = bodyInput.textContent;
|
||||
}
|
||||
|
||||
WorkspaceManager.saveRequestToCollection(workspace.id, collection.id, saved);
|
||||
alert(`Request saved to "${collection.name}"`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyboard shortcuts and accessibility
|
||||
*/
|
||||
function initializeKeyboardShortcuts() {
|
||||
const shortcuts = {
|
||||
'/': () => document.getElementById('searchInput')?.focus(),
|
||||
'KeyS': (e) => e.ctrlKey && saveCurrentRequest(),
|
||||
'KeyK': (e) => e.ctrlKey && toggleCommandPalette(),
|
||||
'Tab': (e) => handleTabNavigation(e),
|
||||
'Escape': () => closeModals()
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Don't trigger shortcuts when typing in input fields
|
||||
if (e.target.matches('input, textarea, [contenteditable]')) {
|
||||
if (e.key === 'Escape') {
|
||||
e.target.blur();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = shortcuts[e.code] || shortcuts[e.key];
|
||||
if (handler) {
|
||||
handler(e);
|
||||
}
|
||||
});
|
||||
|
||||
// Show keyboard shortcut hints
|
||||
addShortcutHints();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add visual keyboard shortcut hints to UI
|
||||
*/
|
||||
function addShortcutHints() {
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
if (searchInput && !searchInput.parentElement.querySelector('.kbd')) {
|
||||
const kbd = document.createElement('kbd');
|
||||
kbd.className = 'kbd search-kbd';
|
||||
kbd.textContent = '/';
|
||||
searchInput.parentElement.appendChild(kbd);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle command palette (future enhancement)
|
||||
*/
|
||||
function toggleCommandPalette() {
|
||||
// Placeholder for future command palette implementation
|
||||
console.log('Command palette not yet implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Tab navigation for better accessibility
|
||||
*/
|
||||
function handleTabNavigation(e) {
|
||||
const focusableElements = document.querySelectorAll(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
|
||||
if (focusableElements.length === 0) return;
|
||||
|
||||
const currentIndex = Array.from(focusableElements).indexOf(document.activeElement);
|
||||
let nextIndex = e.shiftKey ? currentIndex - 1 : currentIndex + 1;
|
||||
|
||||
if (nextIndex >= focusableElements.length) nextIndex = 0;
|
||||
if (nextIndex < 0) nextIndex = focusableElements.length - 1;
|
||||
|
||||
const nextElement = focusableElements[nextIndex];
|
||||
if (nextElement) {
|
||||
e.preventDefault();
|
||||
nextElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all open modals
|
||||
*/
|
||||
function closeModals() {
|
||||
document.querySelectorAll('[role="dialog"], .modal, .drawer').forEach(modal => {
|
||||
modal.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhance accessibility with ARIA labels
|
||||
*/
|
||||
function enhanceAccessibility() {
|
||||
// Add ARIA roles and labels
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
if (sidebar) sidebar.setAttribute('role', 'navigation');
|
||||
|
||||
const detailPane = document.getElementById('detailPane');
|
||||
if (detailPane) detailPane.setAttribute('role', 'main');
|
||||
|
||||
const tryPane = document.getElementById('tryPane');
|
||||
if (tryPane) tryPane.setAttribute('role', 'complementary');
|
||||
|
||||
// Add aria-labels to icon buttons
|
||||
document.querySelectorAll('.icon-btn').forEach(btn => {
|
||||
if (!btn.getAttribute('aria-label')) {
|
||||
btn.setAttribute('aria-label', btn.title || btn.textContent);
|
||||
}
|
||||
});
|
||||
|
||||
// Add roles to custom components
|
||||
document.querySelectorAll('.param-group-header').forEach(header => {
|
||||
header.setAttribute('role', 'button');
|
||||
header.setAttribute('tabindex', '0');
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-overview], [data-endpoint]').forEach(link => {
|
||||
link.setAttribute('role', 'menuitem');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add focus visible styles for keyboard navigation
|
||||
*/
|
||||
function initializeFocusStyles() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
button:focus-visible,
|
||||
a:focus-visible,
|
||||
input:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced error handling and user feedback
|
||||
*/
|
||||
function showNotification(message, type = 'info', duration = 3000) {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification notification-${type}`;
|
||||
notification.setAttribute('role', 'status');
|
||||
notification.setAttribute('aria-live', 'polite');
|
||||
notification.textContent = message;
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 16px;
|
||||
background: var(--bg-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-1);
|
||||
z-index: 9999;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
notification.style.animation = 'slideOut 0.3s ease-out';
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved error logging
|
||||
*/
|
||||
const originalConsoleError = console.error;
|
||||
console.error = function(...args) {
|
||||
originalConsoleError.apply(console, args);
|
||||
const message = args[0]?.toString?.() || String(args[0]);
|
||||
if (message && !message.includes('HTTP')) {
|
||||
showNotification(`Error: ${message.substring(0, 50)}`, 'error', 5000);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize all enhancements
|
||||
*/
|
||||
function initializeEnhancements() {
|
||||
initializeWorkspaceManager();
|
||||
initializeKeyboardShortcuts();
|
||||
enhanceAccessibility();
|
||||
initializeFocusStyles();
|
||||
}
|
||||
|
||||
// Call on load
|
||||
initializeEnhancements();
|
||||
|
||||
init();
|
||||
})();
|
||||
Reference in New Issue
Block a user