From 96ce9d99ad38db62407c5583f74354617b88676e Mon Sep 17 00:00:00 2001 From: Yash-Raj-5424 Date: Fri, 1 May 2026 02:37:29 +0530 Subject: [PATCH 01/12] feat: REST API request execution from docs for GET requests Signed-off-by: Yash-Raj-5424 --- assets/scss/rest-api-interactive.scss | 440 ++++++++++++++++ layouts/docs/rest-apis.html | 2 + layouts/partials/rest-apis/auth-input.html | 26 + .../partials/rest-apis/execute-button.html | 15 + .../partials/rest-apis/parameter-inputs.html | 76 +++ .../partials/rest-apis/server-selector.html | 21 + layouts/partials/rest-apis/url-preview.html | 12 + layouts/partials/rest-apis/viewer.html | 23 +- static/js/rest-api-executor.js | 481 ++++++++++++++++++ 9 files changed, 1095 insertions(+), 1 deletion(-) create mode 100644 assets/scss/rest-api-interactive.scss create mode 100644 layouts/partials/rest-apis/auth-input.html create mode 100644 layouts/partials/rest-apis/execute-button.html create mode 100644 layouts/partials/rest-apis/parameter-inputs.html create mode 100644 layouts/partials/rest-apis/server-selector.html create mode 100644 layouts/partials/rest-apis/url-preview.html create mode 100644 static/js/rest-api-executor.js diff --git a/assets/scss/rest-api-interactive.scss b/assets/scss/rest-api-interactive.scss new file mode 100644 index 0000000000..8d3b571683 --- /dev/null +++ b/assets/scss/rest-api-interactive.scss @@ -0,0 +1,440 @@ +/** + * REST API Interactive Components Styling + * Handles UI for request builder and response display + */ + +/* Execute Button */ +.rest-api-execute-button { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 0.375rem; + font-weight: 600; + font-size: 0.95rem; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 6px -1px rgba(102, 126, 234, 0.3); +} + +.rest-api-execute-button:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 6px 12px -1px rgba(102, 126, 234, 0.4); +} + +.rest-api-execute-button:active:not(:disabled) { + transform: translateY(0); +} + +.rest-api-execute-button:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.rest-api-execute-button.is-loading { + position: relative; + color: transparent; +} + +.rest-api-execute-button.is-loading::after { + content: ''; + position: absolute; + width: 16px; + height: 16px; + top: 50%; + left: 50%; + margin-left: -8px; + margin-top: -8px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: white; + animation: spin 0.6s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Server Selection */ +.rest-api-server-selector { + margin: 1rem 0; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.rest-api-server-selector label { + font-weight: 600; + font-size: 0.9rem; + color: #374151; +} + +.rest-api-server-select { + padding: 0.5rem 0.75rem; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + background-color: white; + font-size: 0.9rem; + cursor: pointer; + transition: border-color 0.2s ease; +} + +.rest-api-server-select:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +/* URL Preview */ +.rest-api-url-preview { + margin: 1rem 0; + padding: 0.75rem 1rem; + background-color: #f3f4f6; + border-left: 4px solid #667eea; + border-radius: 0.375rem; + font-family: 'Monaco', 'Courier New', monospace; + font-size: 0.85rem; + word-break: break-all; + color: #374151; +} + +/* Parameter Inputs */ +.rest-api-parameter-group { + margin: 1rem 0; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.rest-api-parameter-input { + display: flex; + flex-direction: column; + gap: 0.375rem; +} + +.rest-api-parameter-input label { + font-weight: 600; + font-size: 0.85rem; + color: #374151; +} + +.rest-api-parameter-input label .rest-api-parameter-required { + color: #dc2626; + margin-left: 0.25rem; +} + +.rest-api-parameter-input input, +.rest-api-parameter-input textarea { + padding: 0.5rem 0.75rem; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + font-size: 0.9rem; + font-family: inherit; + transition: border-color 0.2s ease; +} + +.rest-api-parameter-input input:focus, +.rest-api-parameter-input textarea:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.rest-api-parameter-input .rest-api-parameter-description { + font-size: 0.8rem; + color: #6b7280; + margin-top: -0.25rem; +} + +/* Authentication */ +.rest-api-auth-input { + margin: 1rem 0; + padding: 1rem; + background-color: #fef3c7; + border-left: 4px solid #f59e0b; + border-radius: 0.375rem; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.rest-api-auth-input label { + font-weight: 600; + font-size: 0.9rem; + color: #92400e; +} + +.rest-api-auth-input input { + padding: 0.5rem 0.75rem; + border: 1px solid #fcd34d; + border-radius: 0.375rem; + font-size: 0.9rem; + background-color: rgba(255, 255, 255, 0.8); +} + +.rest-api-auth-input input:focus { + outline: none; + border-color: #f59e0b; + box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.1); +} + +.rest-api-auth-input .rest-api-auth-help { + font-size: 0.8rem; + color: #92400e; +} + +/* Response Display */ +.rest-api-response-display { + margin-top: 2rem; + padding: 1.5rem; + background-color: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 0.5rem; + display: none; + animation: slideIn 0.3s ease; +} + +.rest-api-response-display--active { + display: block; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.rest-api-response-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding-bottom: 0.75rem; + border-bottom: 2px solid #e5e7eb; +} + +.rest-api-response-header h4 { + margin: 0; + font-size: 1.1rem; + font-weight: 600; + color: #111827; +} + +.rest-api-response-meta { + display: flex; + gap: 1rem; + align-items: center; + font-size: 0.9rem; +} + +.rest-api-response-status { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.35rem 0.75rem; + border-radius: 0.375rem; + font-weight: 600; + font-family: 'Monaco', 'Courier New', monospace; + font-size: 0.85rem; +} + +.rest-api-response-status.success { + background-color: #d1fae5; + color: #065f46; +} + +.rest-api-response-status.error { + background-color: #fee2e2; + color: #991b1b; +} + +.rest-api-response-status.loading { + background-color: #dbeafe; + color: #0c4a6e; +} + +.rest-api-response-duration { + color: #6b7280; + font-size: 0.85rem; +} + +/* Response Body */ +.rest-api-response-body { + margin: 0; + padding: 1rem; + background-color: #1f2937; + border-radius: 0.375rem; + color: #e5e7eb; + font-size: 0.85rem; + font-family: 'Monaco', 'Courier New', monospace; + overflow-x: auto; + max-height: 400px; + overflow-y: auto; +} + +.rest-api-response-body code { + background: none; + color: inherit; + padding: 0; + font-family: inherit; + font-size: inherit; +} + +.rest-api-response-body.language-json { + color: #60a5fa; +} + +/* Syntax highlighting for JSON */ +.rest-api-response-body.hljs { + background: #1f2937; + padding: 1rem; +} + +/* Error Display */ +.rest-api-error-display { + margin-top: 2rem; + padding: 1.5rem; + background-color: #fef2f2; + border: 1px solid #fecaca; + border-left: 4px solid #dc2626; + border-radius: 0.5rem; + animation: slideIn 0.3s ease; +} + +.rest-api-error-display--active { + display: block; +} + +.rest-api-error-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 1rem; +} + +.rest-api-error-icon { + font-size: 1.5rem; +} + +.rest-api-error-header h4 { + margin: 0; + font-size: 1.1rem; + font-weight: 600; + color: #991b1b; +} + +.rest-api-error-message { + margin: 0 0 0.75rem 0; + color: #7f1d1d; + font-size: 0.95rem; +} + +.rest-api-error-details { + margin: 0.75rem 0 0 0; +} + +.rest-api-error-details summary { + cursor: pointer; + font-weight: 600; + color: #991b1b; + padding: 0.5rem; + user-select: none; +} + +.rest-api-error-details summary:hover { + background-color: rgba(220, 38, 38, 0.1); + border-radius: 0.375rem; +} + +.rest-api-error-details pre { + margin: 0.75rem 0 0 0; + padding: 0.75rem; + background-color: rgba(220, 38, 38, 0.05); + border-radius: 0.375rem; + font-size: 0.8rem; + overflow-x: auto; + font-family: 'Monaco', 'Courier New', monospace; + color: #7f1d1d; +} + +/* Request Section */ +.rest-api-section--try .rest-api-execute-section { + margin-top: 1.5rem; + padding-top: 1.5rem; + border-top: 1px solid #e5e7eb; +} + +.rest-api-execute-section h4 { + margin: 0 0 1rem 0; + font-size: 1rem; + font-weight: 600; + color: #111827; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .rest-api-response-meta { + flex-direction: column; + gap: 0.5rem; + align-items: flex-start; + } + + .rest-api-response-body { + max-height: 300px; + font-size: 0.75rem; + } + + .rest-api-execute-button { + width: 100%; + justify-content: center; + } +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .rest-api-response-display { + background-color: #111827; + border-color: #374151; + } + + .rest-api-response-header { + border-bottom-color: #374151; + color: #f3f4f6; + } + + .rest-api-response-header h4 { + color: #f3f4f6; + } + + .rest-api-response-status { + background-color: #064e3b; + color: #d1fae5; + } + + .rest-api-url-preview { + background-color: #1f2937; + border-left-color: #60a5fa; + color: #e5e7eb; + } + + .rest-api-parameter-input input, + .rest-api-parameter-input textarea { + background-color: #1f2937; + border-color: #374151; + color: #f3f4f6; + } + + .rest-api-parameter-input label { + color: #e5e7eb; + } +} diff --git a/layouts/docs/rest-apis.html b/layouts/docs/rest-apis.html index 27518e30cb..49dae381b4 100644 --- a/layouts/docs/rest-apis.html +++ b/layouts/docs/rest-apis.html @@ -21,6 +21,8 @@

{{ .Title }}

{{ partial "page-meta-lastmod.html" . }} + + {{ partial "video-section-related.html" . -}}
{{ partial "recent-discussions.html" . -}} diff --git a/layouts/partials/rest-apis/auth-input.html b/layouts/partials/rest-apis/auth-input.html new file mode 100644 index 0000000000..0fdd096e7b --- /dev/null +++ b/layouts/partials/rest-apis/auth-input.html @@ -0,0 +1,26 @@ +{{/* + Authentication Input Partial + Provides bearer token input for authenticated requests + Context expects: operationId, requiresAuth +*/}} +{{ if .requiresAuth }} +
+ + +

+ Provide a security token for authentication. + + Learn how to generate tokens + +

+
+{{ end }} diff --git a/layouts/partials/rest-apis/execute-button.html b/layouts/partials/rest-apis/execute-button.html new file mode 100644 index 0000000000..de0832fa56 --- /dev/null +++ b/layouts/partials/rest-apis/execute-button.html @@ -0,0 +1,15 @@ +{{/* + Execute Button Partial + Provides the button to trigger API request execution + Context expects: operationId +*/}} + diff --git a/layouts/partials/rest-apis/parameter-inputs.html b/layouts/partials/rest-apis/parameter-inputs.html new file mode 100644 index 0000000000..e741686434 --- /dev/null +++ b/layouts/partials/rest-apis/parameter-inputs.html @@ -0,0 +1,76 @@ +{{/* + Parameter Input Partial + Renders interactive input fields for API parameters + Context expects: parameters (array), operationId +*/}} +{{ if gt (len .parameters) 0 }} + {{ $locations := slice + (dict "key" "path" "label" "Path parameters") + (dict "key" "query" "label" "Query parameters") + (dict "key" "header" "label" "Header parameters") + (dict "key" "cookie" "label" "Cookie parameters") + }} + + {{ range $locations }} + {{ $rows := where $.parameters "in" .key }} + {{ if gt (len $rows) 0 }} +
+

{{ .label }}

+ {{ range $rows }} + {{ $paramSchema := index . "schema" | default dict }} + {{ $paramType := partial "rest-apis/schema-type.html" (dict "schema" $paramSchema "schemas" $.schemas) | strings.TrimSpace }} + {{ $isRequired := .required | default false }} + {{ $paramId := printf "param-%s-%s-%s" $.operationId .in .name }} + +
+ + + {{ if eq $paramType "boolean" }} + + {{ else if eq $paramType "integer" }} + + {{ else }} + + {{ end }} + + {{ with .description }} +

{{ . | markdownify }}

+ {{ end }} +
+ {{ end }} +
+ {{ end }} + {{ end }} +{{ end }} diff --git a/layouts/partials/rest-apis/server-selector.html b/layouts/partials/rest-apis/server-selector.html new file mode 100644 index 0000000000..3ae551ae80 --- /dev/null +++ b/layouts/partials/rest-apis/server-selector.html @@ -0,0 +1,21 @@ +{{/* + Server Selector Partial + Allows users to select which API server to target + Context expects: servers, operationId +*/}} +{{ if gt (len .servers) 0 }} +
+ + +
+{{ end }} diff --git a/layouts/partials/rest-apis/url-preview.html b/layouts/partials/rest-apis/url-preview.html new file mode 100644 index 0000000000..8a7d18705b --- /dev/null +++ b/layouts/partials/rest-apis/url-preview.html @@ -0,0 +1,12 @@ +{{/* + URL Preview Partial + Shows the complete URL that will be requested + Context expects: operationId, path, servers +*/}} +
+ {{ if gt (len .servers) 0 }} + {{ index (index .servers 0) "url" }}{{ .path }} + {{ else }} + https://cloud.layer5.io{{ .path }} + {{ end }} +
diff --git a/layouts/partials/rest-apis/viewer.html b/layouts/partials/rest-apis/viewer.html index 5bac898e59..2ae3efd0ad 100644 --- a/layouts/partials/rest-apis/viewer.html +++ b/layouts/partials/rest-apis/viewer.html @@ -372,7 +372,8 @@

Select an endpoint

{{ $servers = $servers | append $defaultCloudServer }} {{ end }} -
+
{{ upper .method }} @@ -409,6 +410,26 @@

Try this endpoint

{{ end }}
+ + {{ if eq .method "get" }} +
+

Execute directly

+ + {{ partial "rest-apis/server-selector.html" (dict "servers" $servers "operationId" $operationId) }} + + {{ partial "rest-apis/url-preview.html" (dict "operationId" $operationId "path" $operationPath "servers" $servers) }} + + {{ if $authSummary }} + {{ partial "rest-apis/auth-input.html" (dict "operationId" $operationId "requiresAuth" true) }} + {{ end }} + + {{ if gt (len $parameters) 0 }} + {{ partial "rest-apis/parameter-inputs.html" (dict "operationId" $operationId "parameters" $parameters "schemas" $schemas) }} + {{ end }} + + {{ partial "rest-apis/execute-button.html" (dict "operationId" $operationId "method" .method "path" $operationPath) }} +
+ {{ end }} {{ if gt (len $parameters) 0 }} diff --git a/static/js/rest-api-executor.js b/static/js/rest-api-executor.js new file mode 100644 index 0000000000..a2e0be5690 --- /dev/null +++ b/static/js/rest-api-executor.js @@ -0,0 +1,481 @@ +/** + * REST API Executor - Handles interactive API requests from documentation + * Supports GET requests with parameter substitution and response formatting + */ + +class RESTAPIExecutor { + constructor() { + this.activeRequest = null; + this.requestCache = new Map(); + this.init(); + } + + /** + * Initialize the executor by setting up event listeners + */ + init() { + this.setupExecuteButtons(); + this.setupParameterInputs(); + } + + /** + * Set up click handlers for execute buttons + */ + setupExecuteButtons() { + const buttons = document.querySelectorAll('[data-execute-request]'); + buttons.forEach(button => { + button.addEventListener('click', (e) => { + e.preventDefault(); + const operationId = button.dataset.operationId; + this.executeRequest(operationId); + }); + }); + } + + /** + * Set up input listeners for real-time parameter updates + */ + setupParameterInputs() { + const inputs = document.querySelectorAll('[data-parameter-input]'); + inputs.forEach(input => { + input.addEventListener('change', () => { + this.updatePreviewUrl(); + }); + input.addEventListener('input', () => { + this.updatePreviewUrl(); + }); + }); + } + + /** + * Execute an API request for the given operation + * @param {string} operationId - The ID of the operation to execute + */ + async executeRequest(operationId) { + const operationPanel = document.querySelector(`[data-operation-id="${operationId}"]`); + if (!operationPanel) { + console.error(`Operation panel not found for ${operationId}`); + return; + } + + try { + // Get operation metadata from data attributes + const operation = this.getOperationMetadata(operationPanel); + + // Build the complete URL with parameters + const url = this.buildRequestUrl(operationPanel, operation); + + // Extract headers (including auth token if provided) + const headers = this.buildRequestHeaders(operationPanel); + + // Show loading state + this.setLoadingState(operationPanel, true); + + // Abort previous request if any + if (this.activeRequest) { + this.activeRequest.abort(); + } + + // Execute the request + const controller = new AbortController(); + this.activeRequest = controller; + + const startTime = performance.now(); + const response = await fetch(url, { + method: 'GET', + headers: headers, + signal: controller.signal, + mode: 'cors', + }); + const endTime = performance.now(); + const duration = endTime - startTime; + + // Parse response + const contentType = response.headers.get('content-type'); + let responseData; + let responseText; + + if (contentType && contentType.includes('application/json')) { + responseText = await response.text(); + try { + responseData = JSON.parse(responseText); + } catch (e) { + responseData = null; + } + } else { + responseText = await response.text(); + responseData = null; + } + + // Display response + this.displayResponse(operationPanel, { + status: response.status, + statusText: response.statusText, + headers: response.headers, + data: responseData, + text: responseText, + duration: duration, + url: url, + success: response.ok, + }); + + this.setLoadingState(operationPanel, false); + } catch (error) { + if (error.name === 'AbortError') { + this.setLoadingState(operationPanel, false); + return; + } + + try { + this.displayError(operationPanel, error); + } catch (displayError) { + console.error('Error displaying error:', displayError); + } + this.setLoadingState(operationPanel, false); + } + } + + /** + * Get operation metadata from the panel + * @param {Element} operationPanel - The operation panel element + * @returns {Object} Operation metadata + */ + getOperationMetadata(operationPanel) { + return { + id: operationPanel.dataset.operationId, + pathId: operationPanel.dataset.pathId, + method: this.extractMethod(operationPanel), + path: this.extractPath(operationPanel), + servers: this.extractServers(operationPanel), + }; + } + + /** + * Extract HTTP method from panel + * @param {Element} operationPanel - The operation panel element + * @returns {string} HTTP method + */ + extractMethod(operationPanel) { + const badge = operationPanel.querySelector('.rest-api-method-badge'); + return badge ? badge.textContent.toLowerCase() : 'get'; + } + + /** + * Extract path from panel + * @param {Element} operationPanel - The operation panel element + * @returns {string} API path + */ + extractPath(operationPanel) { + const pathElement = operationPanel.querySelector('.rest-api-operation__path'); + return pathElement ? pathElement.textContent.trim() : ''; + } + + /** + * Extract available servers from panel + * @param {Element} operationPanel - The operation panel element + * @returns {Array} Server URLs + */ + extractServers(operationPanel) { + const trySection = operationPanel.querySelector('.rest-api-section--try'); + if (!trySection) return ['https://cloud.layer5.io']; + + const servers = []; + const urlElements = trySection.querySelectorAll('.rest-api-try-item__url'); + urlElements.forEach(el => { + const url = el.textContent.trim(); + const baseUrl = url.replace(/\/[^/]*$/, ''); // Remove path, keep base + if (!servers.includes(baseUrl)) { + servers.push(baseUrl); + } + }); + + return servers.length > 0 ? servers : ['https://cloud.layer5.io']; + } + + /** + * Build the complete request URL with parameters + * @param {Element} operationPanel - The operation panel element + * @param {Object} operation - Operation metadata + * @returns {string} Complete URL + */ + buildRequestUrl(operationPanel, operation) { + let path = operation.path; + let baseUrl = this.getSelectedServer(operationPanel) || operation.servers[0]; + + // Replace path parameters + const pathParams = this.getParameterValues(operationPanel, 'path'); + Object.entries(pathParams).forEach(([name, value]) => { + path = path.replace(`{${name}}`, encodeURIComponent(value)); + }); + + // Build query string + const queryParams = this.getParameterValues(operationPanel, 'query'); + const queryString = this.buildQueryString(queryParams); + + return `${baseUrl}${path}${queryString}`; + } + + /** + * Build query string from parameters + * @param {Object} params - Query parameters + * @returns {string} Query string + */ + buildQueryString(params) { + const entries = Object.entries(params) + .filter(([, value]) => value !== null && value !== '') + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`); + + return entries.length > 0 ? `?${entries.join('&')}` : ''; + } + + /** + * Get parameter values from inputs + * @param {Element} operationPanel - The operation panel element + * @param {string} location - Parameter location (path, query, header, cookie) + * @returns {Object} Parameter values + */ + getParameterValues(operationPanel, location) { + const params = {}; + const inputs = operationPanel.querySelectorAll( + `[data-parameter-input][data-parameter-location="${location}"]` + ); + + inputs.forEach(input => { + const name = input.dataset.parameterName; + if (name) { + params[name] = input.value; + } + }); + + return params; + } + + /** + * Get the selected server URL + * @param {Element} operationPanel - The operation panel element + * @returns {string|null} Selected server URL + */ + getSelectedServer(operationPanel) { + const select = operationPanel.querySelector('[data-server-select]'); + return select ? select.value : null; + } + + /** + * Build request headers + * @param {Element} operationPanel - The operation panel element + * @returns {Object} Headers object + */ + buildRequestHeaders(operationPanel) { + const headers = { + 'Accept': 'application/json', + }; + + // Add custom headers if provided + const headerInputs = this.getParameterValues(operationPanel, 'header'); + Object.assign(headers, headerInputs); + + // Add authorization token if provided + const authInput = operationPanel.querySelector('[data-auth-token-input]'); + if (authInput && authInput.value) { + headers['Authorization'] = `Bearer ${authInput.value}`; + } + + return headers; + } + + /** + * Update the preview URL when parameters change + */ + updatePreviewUrl() { + const panels = document.querySelectorAll('[data-operation-panel]'); + panels.forEach(panel => { + const preview = panel.querySelector('[data-url-preview]'); + if (preview) { + const operation = this.getOperationMetadata(panel); + const url = this.buildRequestUrl(panel, operation); + preview.textContent = url; + } + }); + } + + /** + * Display API response in the UI + * @param {Element} operationPanel - The operation panel element + * @param {Object} response - Response data + */ + displayResponse(operationPanel, response) { + try { + let responseSection = operationPanel.querySelector('.rest-api-response-display'); + + if (!responseSection) { + responseSection = this.createResponseSection(operationPanel); + } + + // Update status + const statusElement = responseSection.querySelector('[data-response-status]'); + if (statusElement) { + statusElement.textContent = `${response.status} ${response.statusText}`; + statusElement.className = `rest-api-response-status ${response.success ? 'success' : 'error'}`; + } + + // Update duration + const durationElement = responseSection.querySelector('[data-response-duration]'); + if (durationElement) { + durationElement.textContent = `${response.duration.toFixed(0)}ms`; + } + + // Update body + const bodyElement = responseSection.querySelector('[data-response-body]'); + if (bodyElement) { + if (response.data) { + bodyElement.textContent = JSON.stringify(response.data, null, 2); + bodyElement.classList.add('language-json'); + } else if (response.text) { + bodyElement.textContent = response.text; + } else { + bodyElement.textContent = '(empty response)'; + } + + // Re-highlight if using a syntax highlighter + if (window.hljs) { + window.hljs.highlightElement(bodyElement); + } + } + + responseSection.style.display = 'block'; + } catch (err) { + console.error('Error displaying response:', err); + } + } + + /** + * Create the response display section + * @param {Element} operationPanel - The operation panel element + * @returns {Element} Response section + */ + createResponseSection(operationPanel) { + try { + const section = document.createElement('section'); + section.className = 'rest-api-response-display'; + section.innerHTML = ` +
+

Response

+
+ 200 OK + 0ms +
+
+
+ `; + + // Find insertion point (after request section or at end) + const requestSection = operationPanel.querySelector('.rest-api-section--try') || + operationPanel.lastElementChild; + if (requestSection) { + requestSection.after(section); + } else { + operationPanel.appendChild(section); + } + + return section; + } catch (err) { + console.error('Error creating response section:', err); + throw err; + } + } + + /** + * Display an error message + * @param {Element} operationPanel - The operation panel element + * @param {Error} error - The error object + */ + displayError(operationPanel, error) { + const errorMessage = error.message || 'An error occurred while executing the request'; + const detailedMessage = this.getDetailedError(error); + + const errorSection = document.createElement('div'); + errorSection.className = 'rest-api-error-display'; + errorSection.innerHTML = ` +
+ ⚠️ +

Error

+
+

${this.escapeHtml(errorMessage)}

+ ${detailedMessage ? `
+ Details +
${this.escapeHtml(detailedMessage)}
+
` : ''} + `; + + // Replace or create response section + let responseSection = operationPanel.querySelector('.rest-api-response-display'); + if (responseSection) { + responseSection.replaceWith(errorSection); + } else { + const requestSection = operationPanel.querySelector('.rest-api-section--try'); + if (requestSection) { + requestSection.after(errorSection); + } else { + operationPanel.appendChild(errorSection); + } + } + + errorSection.classList.add('rest-api-error-display--active'); + } + + /** + * Get detailed error information + * @param {Error} error - The error object + * @returns {string} Detailed error message + */ + getDetailedError(error) { + if (error instanceof TypeError && error.message.includes('Failed to fetch')) { + return 'CORS Error: The API server may not allow requests from this domain. Try using a CORS proxy or configuring CORS headers on the API server.'; + } + + if (error.response && error.response.statusText) { + return `${error.response.status} ${error.response.statusText}`; + } + + return error.stack || error.message; + } + + /** + * Set loading state + * @param {Element} operationPanel - The operation panel element + * @param {boolean} isLoading - Whether loading + */ + setLoadingState(operationPanel, isLoading) { + const button = operationPanel.querySelector('[data-execute-request]'); + if (button) { + button.disabled = isLoading; + button.classList.toggle('is-loading', isLoading); + button.textContent = isLoading ? 'Executing...' : 'Execute Request'; + } + } + + /** + * Escape HTML to prevent XSS + * @param {string} text - Text to escape + * @returns {string} Escaped text + */ + escapeHtml(text) { + const map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + }; + return text.replace(/[&<>"']/g, m => map[m]); + } +} + +// Initialize when DOM is ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + window.restAPIExecutor = new RESTAPIExecutor(); + }); +} else { + window.restAPIExecutor = new RESTAPIExecutor(); +} From 6df97078a94b0c4ce610d713a1a16778175aedcb Mon Sep 17 00:00:00 2001 From: Yash-Raj-5424 Date: Sun, 3 May 2026 08:49:03 +0530 Subject: [PATCH 02/12] fix: handle multi-segment API path by passing base URL from data attributes Signed-off-by: Yash-Raj-5424 --- layouts/partials/rest-apis/viewer.html | 2 +- static/js/rest-api-executor.js | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/layouts/partials/rest-apis/viewer.html b/layouts/partials/rest-apis/viewer.html index 2ae3efd0ad..de59bce509 100644 --- a/layouts/partials/rest-apis/viewer.html +++ b/layouts/partials/rest-apis/viewer.html @@ -404,7 +404,7 @@

Try this endpoint

{{ range $server := $servers }} {{ $serverUrl := index $server "url" | default "" }} {{ $serverDescription := index $server "description" | default "Layer5 Cloud URL" }} -
+

{{ $serverDescription }}

{{ printf "%s%s" $serverUrl $operationPath }}
diff --git a/static/js/rest-api-executor.js b/static/js/rest-api-executor.js index a2e0be5690..0d7c9b3a89 100644 --- a/static/js/rest-api-executor.js +++ b/static/js/rest-api-executor.js @@ -180,11 +180,10 @@ class RESTAPIExecutor { if (!trySection) return ['https://cloud.layer5.io']; const servers = []; - const urlElements = trySection.querySelectorAll('.rest-api-try-item__url'); - urlElements.forEach(el => { - const url = el.textContent.trim(); - const baseUrl = url.replace(/\/[^/]*$/, ''); // Remove path, keep base - if (!servers.includes(baseUrl)) { + const tryItems = trySection.querySelectorAll('.rest-api-try-item'); + tryItems.forEach(item => { + const baseUrl = item.dataset.baseUrl; + if (baseUrl && !servers.includes(baseUrl)) { servers.push(baseUrl); } }); From 70588f3eb4557e6d1699b58c483acff0a2df3b01 Mon Sep 17 00:00:00 2001 From: Yash-Raj-5424 Date: Sun, 3 May 2026 09:30:01 +0530 Subject: [PATCH 03/12] refactor: read HTTP method from data attribute instead of DOM scraping Signed-off-by: Yash-Raj-5424 --- static/js/rest-api-executor.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/static/js/rest-api-executor.js b/static/js/rest-api-executor.js index 0d7c9b3a89..0f598ee012 100644 --- a/static/js/rest-api-executor.js +++ b/static/js/rest-api-executor.js @@ -156,8 +156,7 @@ class RESTAPIExecutor { * @returns {string} HTTP method */ extractMethod(operationPanel) { - const badge = operationPanel.querySelector('.rest-api-method-badge'); - return badge ? badge.textContent.toLowerCase() : 'get'; + return operationPanel.dataset.operationMethod || 'get'; } /** From 7a7dc6e7d8304706015e0011abedaa89cd58b181 Mon Sep 17 00:00:00 2001 From: Yash-Raj-5424 Date: Sun, 3 May 2026 09:55:56 +0530 Subject: [PATCH 04/12] perf: optimize URL preview updates using event delegation and targeted panel updates Signed-off-by: Yash-Raj-5424 --- static/js/rest-api-executor.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/static/js/rest-api-executor.js b/static/js/rest-api-executor.js index 0f598ee012..3b231d7ad2 100644 --- a/static/js/rest-api-executor.js +++ b/static/js/rest-api-executor.js @@ -33,17 +33,20 @@ class RESTAPIExecutor { } /** - * Set up input listeners for real-time parameter updates + * Set up input listeners for real-time parameter updates using event delegation */ setupParameterInputs() { - const inputs = document.querySelectorAll('[data-parameter-input]'); - inputs.forEach(input => { - input.addEventListener('change', () => { - this.updatePreviewUrl(); - }); - input.addEventListener('input', () => { - this.updatePreviewUrl(); - }); + document.addEventListener('change', (e) => { + if (e.target.matches('[data-parameter-input]')) { + const panel = e.target.closest('[data-operation-panel]'); + if (panel) this.updatePreviewUrl(panel); + } + }); + document.addEventListener('input', (e) => { + if (e.target.matches('[data-parameter-input]')) { + const panel = e.target.closest('[data-operation-panel]'); + if (panel) this.updatePreviewUrl(panel); + } }); } @@ -283,9 +286,10 @@ class RESTAPIExecutor { /** * Update the preview URL when parameters change + * @param {Element} operationPanel - Optional specific panel to update. If not provided, updates all panels. */ - updatePreviewUrl() { - const panels = document.querySelectorAll('[data-operation-panel]'); + updatePreviewUrl(operationPanel) { + const panels = operationPanel ? [operationPanel] : document.querySelectorAll('[data-operation-panel]'); panels.forEach(panel => { const preview = panel.querySelector('[data-url-preview]'); if (preview) { From 41406feed3d7d87cacedcf96c7509f9d09c9f943 Mon Sep 17 00:00:00 2001 From: Yash-Raj-5424 Date: Sun, 3 May 2026 09:59:38 +0530 Subject: [PATCH 05/12] fix: include rest-api-interactive.scss in build pipeline Signed-off-by: Yash-Raj-5424 --- assets/scss/_styles_project.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/scss/_styles_project.scss b/assets/scss/_styles_project.scss index c686003729..f8e04e6e55 100644 --- a/assets/scss/_styles_project.scss +++ b/assets/scss/_styles_project.scss @@ -11,6 +11,7 @@ @import "_videos_project.scss"; @import "subscription.scss"; @import "rest-api-reference.scss"; +@import "rest-api-interactive.scss"; @import "_video-landing_project.scss"; @import "elements_project"; @import "summary.scss"; From 4202c622a2b084450f8f8a1f006623e54dd4a3aa Mon Sep 17 00:00:00 2001 From: Yash-Raj-5424 Date: Sun, 3 May 2026 10:00:58 +0530 Subject: [PATCH 06/12] security: add rel=noopener noreferrer to external link preventing reverse-tabnabbing Signed-off-by: Yash-Raj-5424 --- layouts/partials/rest-apis/auth-input.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/layouts/partials/rest-apis/auth-input.html b/layouts/partials/rest-apis/auth-input.html index 0fdd096e7b..4d7045547b 100644 --- a/layouts/partials/rest-apis/auth-input.html +++ b/layouts/partials/rest-apis/auth-input.html @@ -18,7 +18,7 @@ />

Provide a security token for authentication. - + Learn how to generate tokens

From 2d5e4c81e724dc0a7593f70989e455670e40d1e1 Mon Sep 17 00:00:00 2001 From: Yash-Raj-5424 Date: Sun, 3 May 2026 10:04:17 +0530 Subject: [PATCH 07/12] fix: update URL preview when API server select changes Signed-off-by: Yash-Raj-5424 --- static/js/rest-api-executor.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/static/js/rest-api-executor.js b/static/js/rest-api-executor.js index 3b231d7ad2..0b344365e8 100644 --- a/static/js/rest-api-executor.js +++ b/static/js/rest-api-executor.js @@ -37,9 +37,11 @@ class RESTAPIExecutor { */ setupParameterInputs() { document.addEventListener('change', (e) => { - if (e.target.matches('[data-parameter-input]')) { - const panel = e.target.closest('[data-operation-panel]'); - if (panel) this.updatePreviewUrl(panel); + const panel = e.target.closest('[data-operation-panel]'); + if (panel) { + if (e.target.matches('[data-parameter-input]') || e.target.matches('[data-server-select]')) { + this.updatePreviewUrl(panel); + } } }); document.addEventListener('input', (e) => { From 32e66719acbf4d009581e5aa8e077cc1d282d064 Mon Sep 17 00:00:00 2001 From: Yash-Raj-5424 Date: Sun, 3 May 2026 10:09:38 +0530 Subject: [PATCH 08/12] refactor: read server URLs from dropdown instead of parsing displayed URLs Signed-off-by: Yash-Raj-5424 --- static/js/rest-api-executor.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/static/js/rest-api-executor.js b/static/js/rest-api-executor.js index 0b344365e8..130adbceab 100644 --- a/static/js/rest-api-executor.js +++ b/static/js/rest-api-executor.js @@ -177,18 +177,18 @@ class RESTAPIExecutor { /** * Extract available servers from panel * @param {Element} operationPanel - The operation panel element - * @returns {Array} Server URLs + * @returns {Array} Server URLs from the server selector dropdown */ extractServers(operationPanel) { - const trySection = operationPanel.querySelector('.rest-api-section--try'); - if (!trySection) return ['https://cloud.layer5.io']; + const serverSelect = operationPanel.querySelector('[data-server-select]'); + if (!serverSelect) return ['https://cloud.layer5.io']; const servers = []; - const tryItems = trySection.querySelectorAll('.rest-api-try-item'); - tryItems.forEach(item => { - const baseUrl = item.dataset.baseUrl; - if (baseUrl && !servers.includes(baseUrl)) { - servers.push(baseUrl); + const options = serverSelect.querySelectorAll('option'); + options.forEach(option => { + const url = option.value; + if (url && !servers.includes(url)) { + servers.push(url); } }); From 30ca35cc80674989e87efd29e3808a293eac9b2e Mon Sep 17 00:00:00 2001 From: Yash-Raj-5424 Date: Sun, 3 May 2026 10:15:17 +0530 Subject: [PATCH 09/12] fix: remove unsupported cookie parameters from interactive form Signed-off-by: Yash-Raj-5424 --- layouts/partials/rest-apis/parameter-inputs.html | 1 - static/js/rest-api-executor.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/layouts/partials/rest-apis/parameter-inputs.html b/layouts/partials/rest-apis/parameter-inputs.html index e741686434..39b963f004 100644 --- a/layouts/partials/rest-apis/parameter-inputs.html +++ b/layouts/partials/rest-apis/parameter-inputs.html @@ -8,7 +8,6 @@ (dict "key" "path" "label" "Path parameters") (dict "key" "query" "label" "Query parameters") (dict "key" "header" "label" "Header parameters") - (dict "key" "cookie" "label" "Cookie parameters") }} {{ range $locations }} diff --git a/static/js/rest-api-executor.js b/static/js/rest-api-executor.js index 130adbceab..b49d805ca0 100644 --- a/static/js/rest-api-executor.js +++ b/static/js/rest-api-executor.js @@ -234,7 +234,7 @@ class RESTAPIExecutor { /** * Get parameter values from inputs * @param {Element} operationPanel - The operation panel element - * @param {string} location - Parameter location (path, query, header, cookie) + * @param {string} location - Parameter location (path, query, or header) * @returns {Object} Parameter values */ getParameterValues(operationPanel, location) { From 2cb7b867c6f5810cf850f3629b610c4498844b8d Mon Sep 17 00:00:00 2001 From: Yash-Raj-5424 Date: Sun, 3 May 2026 10:17:47 +0530 Subject: [PATCH 10/12] docs: update execute button partial comment to reflect required context fields Signed-off-by: Yash-Raj-5424 --- layouts/partials/rest-apis/execute-button.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/layouts/partials/rest-apis/execute-button.html b/layouts/partials/rest-apis/execute-button.html index de0832fa56..2221d7335e 100644 --- a/layouts/partials/rest-apis/execute-button.html +++ b/layouts/partials/rest-apis/execute-button.html @@ -1,7 +1,7 @@ {{/* Execute Button Partial Provides the button to trigger API request execution - Context expects: operationId + Context expects: operationId, method, path */}}