feat(api): improve auth UX with sessionStorage and fix styling issues
Auth credentials: - Switch from in-memory to sessionStorage for credentials - Credentials persist across page navigations within browser tab - Auto-clear when tab closes (no long-term storage) - Pre-fill form fields with saved credentials on page load - Update status text and button based on credential state Styling fixes: - Add right padding to code blocks so Copy button doesn't overlap content - Make long URLs wrap instead of requiring horizontal scroll - Hide TOC sidebar when no headings exist (e.g., quick-start page) - Remove "Use RapiDoc's navigation..." message from TOC API docs: - Remove x-codeSamples from write_lp endpoints (Core and Enterprise) - Add schema descriptions for line protocol request bodyclaude/fix-docs-build-issue-VL3Et
parent
4fccddc255
commit
fa9eda452a
|
|
@ -503,38 +503,6 @@ paths:
|
|||
description: Request entity too large.
|
||||
'422':
|
||||
description: Unprocessable entity.
|
||||
x-codeSamples:
|
||||
- label: cURL - Basic write
|
||||
lang: Shell
|
||||
source: |
|
||||
curl --request POST "http://localhost:8181/api/v3/write_lp?db=sensors" \
|
||||
--header "Authorization: Bearer DATABASE_TOKEN" \
|
||||
--header "Content-Type: text/plain" \
|
||||
--data-raw "cpu,host=server01 usage=85.2 1638360000000000000"
|
||||
- label: cURL - Write with millisecond precision
|
||||
lang: Shell
|
||||
source: |
|
||||
curl --request POST "http://localhost:8181/api/v3/write_lp?db=sensors&precision=ms" \
|
||||
--header "Authorization: Bearer DATABASE_TOKEN" \
|
||||
--header "Content-Type: text/plain" \
|
||||
--data-raw "cpu,host=server01 usage=85.2 1638360000000"
|
||||
- label: cURL - Asynchronous write with partial acceptance
|
||||
lang: Shell
|
||||
source: |
|
||||
curl --request POST "http://localhost:8181/api/v3/write_lp?db=sensors&accept_partial=true&no_sync=true&precision=auto" \
|
||||
--header "Authorization: Bearer DATABASE_TOKEN" \
|
||||
--header "Content-Type: text/plain" \
|
||||
--data-raw "cpu,host=server01 usage=85.2
|
||||
memory,host=server01 used=4096"
|
||||
- label: cURL - Multiple measurements with tags
|
||||
lang: Shell
|
||||
source: |
|
||||
curl --request POST "http://localhost:8181/api/v3/write_lp?db=sensors&precision=ns" \
|
||||
--header "Authorization: Bearer DATABASE_TOKEN" \
|
||||
--header "Content-Type: text/plain" \
|
||||
--data-raw "cpu,host=server01,region=us-west usage=85.2,load=0.75 1638360000000000000
|
||||
memory,host=server01,region=us-west used=4096,free=12288 1638360000000000000
|
||||
disk,host=server01,region=us-west,device=/dev/sda1 used=50.5,free=49.5 1638360000000000000"
|
||||
tags:
|
||||
- Write data
|
||||
/api/v3/query_sql:
|
||||
|
|
@ -1969,16 +1937,23 @@ components:
|
|||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
description: |
|
||||
Line protocol data. Each line represents a point with a measurement name,
|
||||
optional tag set, field set, and optional timestamp.
|
||||
|
||||
Format: `<measurement>[,<tag_key>=<tag_value>...] <field_key>=<field_value>[,<field_key>=<field_value>...] [<timestamp>]`
|
||||
examples:
|
||||
line:
|
||||
summary: Example line protocol
|
||||
value: measurement,tag=value field=1 1234567890
|
||||
multiline:
|
||||
summary: Example line protocol with UTF-8 characters
|
||||
single-point:
|
||||
summary: Write a single point
|
||||
description: Write one point with tags and fields to a table.
|
||||
value: cpu,host=server01 usage=85.2,load=0.75 1638360000000000000
|
||||
multiple-tables:
|
||||
summary: Write to multiple tables
|
||||
description: Write points to different tables (measurements) in a single request.
|
||||
value: |
|
||||
measurement,tag=value field=1 1234567890
|
||||
measurement,tag=value field=2 1234567900
|
||||
measurement,tag=value field=3 1234568000
|
||||
cpu,host=server01,region=us-west usage=85.2,load=0.75 1638360000000000000
|
||||
memory,host=server01,region=us-west used=4096,free=12288 1638360000000000000
|
||||
disk,host=server01,region=us-west,device=/dev/sda1 used=50.5,free=49.5 1638360000000000000
|
||||
queryRequestBody:
|
||||
required: true
|
||||
content:
|
||||
|
|
|
|||
|
|
@ -458,38 +458,6 @@ paths:
|
|||
description: Request entity too large.
|
||||
'422':
|
||||
description: Unprocessable entity.
|
||||
x-codeSamples:
|
||||
- label: cURL - Basic write
|
||||
lang: Shell
|
||||
source: |
|
||||
curl --request POST "http://localhost:8181/api/v3/write_lp?db=sensors" \
|
||||
--header "Authorization: Bearer DATABASE_TOKEN" \
|
||||
--header "Content-Type: text/plain" \
|
||||
--data-raw "cpu,host=server01 usage=85.2 1638360000000000000"
|
||||
- label: cURL - Write with millisecond precision
|
||||
lang: Shell
|
||||
source: |
|
||||
curl --request POST "http://localhost:8181/api/v3/write_lp?db=sensors&precision=ms" \
|
||||
--header "Authorization: Bearer DATABASE_TOKEN" \
|
||||
--header "Content-Type: text/plain" \
|
||||
--data-raw "cpu,host=server01 usage=85.2 1638360000000"
|
||||
- label: cURL - Asynchronous write with partial acceptance
|
||||
lang: Shell
|
||||
source: |
|
||||
curl --request POST "http://localhost:8181/api/v3/write_lp?db=sensors&accept_partial=true&no_sync=true&precision=auto" \
|
||||
--header "Authorization: Bearer DATABASE_TOKEN" \
|
||||
--header "Content-Type: text/plain" \
|
||||
--data-raw "cpu,host=server01 usage=85.2
|
||||
memory,host=server01 used=4096"
|
||||
- label: cURL - Multiple measurements with tags
|
||||
lang: Shell
|
||||
source: |
|
||||
curl --request POST "http://localhost:8181/api/v3/write_lp?db=sensors&precision=ns" \
|
||||
--header "Authorization: Bearer DATABASE_TOKEN" \
|
||||
--header "Content-Type: text/plain" \
|
||||
--data-raw "cpu,host=server01,region=us-west usage=85.2,load=0.75 1638360000000000000
|
||||
memory,host=server01,region=us-west used=4096,free=12288 1638360000000000000
|
||||
disk,host=server01,region=us-west,device=/dev/sda1 used=50.5,free=49.5 1638360000000000000"
|
||||
tags:
|
||||
- Write data
|
||||
/api/v3/query_sql:
|
||||
|
|
@ -2019,16 +1987,23 @@ components:
|
|||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
description: |
|
||||
Line protocol data. Each line represents a point with a measurement name,
|
||||
optional tag set, field set, and optional timestamp.
|
||||
|
||||
Format: `<measurement>[,<tag_key>=<tag_value>...] <field_key>=<field_value>[,<field_key>=<field_value>...] [<timestamp>]`
|
||||
examples:
|
||||
line:
|
||||
summary: Example line protocol
|
||||
value: measurement,tag=value field=1 1234567890
|
||||
multiline:
|
||||
summary: Example line protocol with UTF-8 characters
|
||||
single-point:
|
||||
summary: Write a single point
|
||||
description: Write one point with tags and fields to a table.
|
||||
value: cpu,host=server01 usage=85.2,load=0.75 1638360000000000000
|
||||
multiple-tables:
|
||||
summary: Write to multiple tables
|
||||
description: Write points to different tables (measurements) in a single request.
|
||||
value: |
|
||||
measurement,tag=value field=1 1234567890
|
||||
measurement,tag=value field=2 1234567900
|
||||
measurement,tag=value field=3 1234568000
|
||||
cpu,host=server01,region=us-west usage=85.2,load=0.75 1638360000000000000
|
||||
memory,host=server01,region=us-west used=4096,free=12288 1638360000000000000
|
||||
disk,host=server01,region=us-west,device=/dev/sda1 used=50.5,free=49.5 1638360000000000000
|
||||
queryRequestBody:
|
||||
required: true
|
||||
content:
|
||||
|
|
|
|||
|
|
@ -31,32 +31,43 @@ interface AuthCredentials {
|
|||
|
||||
type CleanupFn = () => void;
|
||||
|
||||
// In-memory credential storage (not persisted)
|
||||
let currentCredentials: AuthCredentials = {};
|
||||
// sessionStorage key for credentials
|
||||
// Persists across page navigations, cleared when tab closes
|
||||
const CREDENTIALS_KEY = 'influxdata-api-credentials';
|
||||
|
||||
/**
|
||||
* Get current credentials (in-memory only)
|
||||
* Get credentials from sessionStorage
|
||||
*/
|
||||
function getCredentials(): AuthCredentials {
|
||||
return currentCredentials;
|
||||
try {
|
||||
const stored = sessionStorage.getItem(CREDENTIALS_KEY);
|
||||
return stored ? JSON.parse(stored) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set credentials (in-memory only, not persisted)
|
||||
* Set credentials in sessionStorage
|
||||
*/
|
||||
function setCredentials(credentials: AuthCredentials): void {
|
||||
currentCredentials = credentials;
|
||||
try {
|
||||
if (Object.keys(credentials).length === 0) {
|
||||
sessionStorage.removeItem(CREDENTIALS_KEY);
|
||||
} else {
|
||||
sessionStorage.setItem(CREDENTIALS_KEY, JSON.stringify(credentials));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[API Auth] Failed to store credentials:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any credentials are set
|
||||
*/
|
||||
function hasCredentials(): boolean {
|
||||
return !!(
|
||||
currentCredentials.bearer ||
|
||||
currentCredentials.basic?.password ||
|
||||
currentCredentials.querystring
|
||||
);
|
||||
const creds = getCredentials();
|
||||
return !!(creds.bearer || creds.basic?.password || creds.querystring);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -115,23 +126,15 @@ function applyCredentialsToRapiDoc(
|
|||
}
|
||||
}
|
||||
|
||||
// Apply bearer/token credentials
|
||||
// Apply bearer/token credentials using setApiKey() only
|
||||
// Using both HTML attributes AND setApiKey() causes "2 API keys applied"
|
||||
if (credentials.bearer) {
|
||||
try {
|
||||
// Method 1: HTML attributes (most reliable)
|
||||
rapiDoc.setAttribute('api-key-name', 'Authorization');
|
||||
rapiDoc.setAttribute('api-key-location', 'header');
|
||||
rapiDoc.setAttribute('api-key-value', `Bearer ${credentials.bearer}`);
|
||||
console.log('[API Auth] Set auth via HTML attributes');
|
||||
|
||||
// Method 2: JavaScript API for scheme-specific auth
|
||||
if ('setApiKey' in rapiDoc) {
|
||||
(rapiDoc as any).setApiKey('BearerAuthentication', credentials.bearer);
|
||||
(rapiDoc as any).setApiKey('TokenAuthentication', credentials.bearer);
|
||||
console.log('[API Auth] Applied bearer/token via setApiKey()');
|
||||
console.log('[API Auth] Applied bearer via setApiKey()');
|
||||
applied = true;
|
||||
}
|
||||
|
||||
applied = true;
|
||||
updateRapiDocAuthInput(rapiDoc, credentials.bearer, 'bearer');
|
||||
} catch (e) {
|
||||
console.error('[API Auth] Failed to set API key:', e);
|
||||
|
|
@ -336,19 +339,30 @@ function updateStatusIndicator(trigger: HTMLElement): void {
|
|||
export default function ApiAuthInput({
|
||||
component,
|
||||
}: ComponentOptions): CleanupFn | void {
|
||||
// Component is the trigger button
|
||||
const trigger = component;
|
||||
const popoverEl = trigger.nextElementSibling as HTMLElement | null;
|
||||
// Component is the banner container, find the trigger button inside it
|
||||
const triggerEl =
|
||||
component.querySelector<HTMLButtonElement>('.api-auth-trigger');
|
||||
const statusEl = component.querySelector<HTMLElement>('.api-auth-status');
|
||||
|
||||
if (!popoverEl || !popoverEl.classList.contains('api-auth-popover')) {
|
||||
if (!triggerEl) {
|
||||
console.error('[API Auth] Trigger button not found in banner');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the popover element (it's a sibling before the banner)
|
||||
const popoverEl = document.querySelector<HTMLElement>('.api-auth-popover');
|
||||
|
||||
if (!popoverEl) {
|
||||
console.error('[API Auth] Popover container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Now TypeScript knows popover is not null
|
||||
// Reassign to new consts so TypeScript knows they're not null in closures
|
||||
const trigger = triggerEl;
|
||||
const popover = popoverEl;
|
||||
|
||||
const schemesAttr = trigger.dataset.schemes || 'bearer';
|
||||
// Get schemes from the component (banner) dataset
|
||||
const schemesAttr = component.dataset.schemes || 'bearer';
|
||||
const schemes = schemesAttr.split(',').map((s) => s.trim().toLowerCase());
|
||||
|
||||
// Render popover content
|
||||
|
|
@ -367,12 +381,35 @@ export default function ApiAuthInput({
|
|||
const clearBtn = popover.querySelector<HTMLButtonElement>('.auth-clear');
|
||||
const closeBtn = popover.querySelector<HTMLButtonElement>('.popover-close');
|
||||
|
||||
// Backdrop element for modal overlay
|
||||
const backdrop = document.querySelector<HTMLElement>('.api-auth-backdrop');
|
||||
|
||||
// Restore saved credentials from sessionStorage
|
||||
const savedCredentials = getCredentials();
|
||||
if (savedCredentials.bearer && bearerInput) {
|
||||
bearerInput.value = savedCredentials.bearer;
|
||||
}
|
||||
if (savedCredentials.basic) {
|
||||
if (usernameInput) usernameInput.value = savedCredentials.basic.username;
|
||||
if (passwordInput) passwordInput.value = savedCredentials.basic.password;
|
||||
}
|
||||
if (savedCredentials.querystring && querystringInput) {
|
||||
querystringInput.value = savedCredentials.querystring;
|
||||
}
|
||||
|
||||
// Update status indicator based on saved credentials
|
||||
if (hasCredentials() && statusEl) {
|
||||
statusEl.textContent = 'Credentials set for this session.';
|
||||
trigger.textContent = 'Update credentials';
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle popover visibility
|
||||
*/
|
||||
function togglePopover(show?: boolean): void {
|
||||
const shouldShow = show ?? popover.hidden;
|
||||
popover.hidden = !shouldShow;
|
||||
if (backdrop) backdrop.hidden = !shouldShow;
|
||||
trigger.setAttribute('aria-expanded', String(shouldShow));
|
||||
|
||||
if (shouldShow) {
|
||||
|
|
@ -401,6 +438,9 @@ export default function ApiAuthInput({
|
|||
// Close button
|
||||
closeBtn?.addEventListener('click', closePopover);
|
||||
|
||||
// Close on backdrop click
|
||||
backdrop?.addEventListener('click', closePopover);
|
||||
|
||||
// Close on outside click
|
||||
function handleOutsideClick(e: MouseEvent): void {
|
||||
if (
|
||||
|
|
@ -462,17 +502,23 @@ export default function ApiAuthInput({
|
|||
setCredentials(newCredentials);
|
||||
updateStatusIndicator(trigger);
|
||||
|
||||
// Update status text and button
|
||||
if (hasCredentials()) {
|
||||
if (statusEl) statusEl.textContent = 'Credentials set for this session.';
|
||||
trigger.textContent = 'Update credentials';
|
||||
}
|
||||
|
||||
// Apply to RapiDoc
|
||||
const rapiDoc = document.querySelector('rapi-doc') as HTMLElement | null;
|
||||
if (rapiDoc && 'setApiKey' in rapiDoc) {
|
||||
const applied = applyCredentialsToRapiDoc(rapiDoc, newCredentials);
|
||||
if (applied) {
|
||||
showFeedback(popover, 'Credentials applied', 'success');
|
||||
showFeedback(popover, 'Credentials saved for this session', 'success');
|
||||
} else {
|
||||
showFeedback(popover, 'No credentials to apply', 'error');
|
||||
}
|
||||
} else {
|
||||
showFeedback(popover, 'Saved (API viewer loading...)', 'success');
|
||||
showFeedback(popover, 'Credentials saved for this session', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -489,19 +535,18 @@ export default function ApiAuthInput({
|
|||
setCredentials({});
|
||||
updateStatusIndicator(trigger);
|
||||
|
||||
// Clear from RapiDoc
|
||||
const rapiDoc = document.querySelector('rapi-doc') as HTMLElement | null;
|
||||
if (rapiDoc) {
|
||||
rapiDoc.removeAttribute('api-key-name');
|
||||
rapiDoc.removeAttribute('api-key-location');
|
||||
rapiDoc.removeAttribute('api-key-value');
|
||||
// Reset status text and button
|
||||
if (statusEl)
|
||||
statusEl.textContent = 'This endpoint requires authentication.';
|
||||
trigger.textContent = 'Set credentials';
|
||||
|
||||
if ('removeAllSecurityKeys' in rapiDoc) {
|
||||
try {
|
||||
(rapiDoc as any).removeAllSecurityKeys();
|
||||
} catch (e) {
|
||||
console.debug('[API Auth] Failed to clear RapiDoc credentials:', e);
|
||||
}
|
||||
// Clear from RapiDoc using removeAllSecurityKeys()
|
||||
const rapiDoc = document.querySelector('rapi-doc') as HTMLElement | null;
|
||||
if (rapiDoc && 'removeAllSecurityKeys' in rapiDoc) {
|
||||
try {
|
||||
(rapiDoc as any).removeAllSecurityKeys();
|
||||
} catch (e) {
|
||||
console.debug('[API Auth] Failed to clear RapiDoc credentials:', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,16 +37,6 @@ interface OperationMeta {
|
|||
tags: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the active panel contains a RapiDoc component
|
||||
*/
|
||||
function isRapiDocActive(): boolean {
|
||||
const activePanel = document.querySelector(
|
||||
'.tab-content:not([style*="display: none"]), [data-tab-panel]:not([style*="display: none"])'
|
||||
);
|
||||
return activePanel?.querySelector('rapi-doc') !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get headings from the currently visible content
|
||||
*/
|
||||
|
|
@ -90,11 +80,8 @@ function getVisibleHeadings(): TocEntry[] {
|
|||
*/
|
||||
function buildTocHtml(entries: TocEntry[]): string {
|
||||
if (entries.length === 0) {
|
||||
// Check if RapiDoc is active - show helpful message
|
||||
if (isRapiDocActive()) {
|
||||
return '<p class="api-toc-empty">Use RapiDoc\'s navigation below to explore this endpoint.</p>';
|
||||
}
|
||||
return '<p class="api-toc-empty">No sections on this page.</p>';
|
||||
// Return empty string - the TOC container can be hidden via CSS when empty
|
||||
return '';
|
||||
}
|
||||
|
||||
let html = '<ul class="api-toc-list">';
|
||||
|
|
@ -405,8 +392,14 @@ export default function ApiToc({ component }: ComponentOptions): void {
|
|||
nav.innerHTML = buildTocHtml(entries);
|
||||
}
|
||||
|
||||
// Set up scroll highlighting
|
||||
observer = setupScrollHighlighting(component, entries);
|
||||
// Hide TOC if no entries, show if entries exist
|
||||
if (entries.length === 0) {
|
||||
component.classList.add('is-hidden');
|
||||
} else {
|
||||
component.classList.remove('is-hidden');
|
||||
// Set up scroll highlighting only when we have entries
|
||||
observer = setupScrollHighlighting(component, entries);
|
||||
}
|
||||
}
|
||||
|
||||
// Check initial visibility (hide for Operations tab, only for non-operations pages)
|
||||
|
|
|
|||
|
|
@ -247,10 +247,7 @@ function createRapiDocElement(
|
|||
// For credential input on operation pages, we need a custom
|
||||
// component (Task 5).
|
||||
//
|
||||
// RECOMMENDATION:
|
||||
// - Keep render-style="read" for compact operation display
|
||||
// - Implement custom auth input component above RapiDoc (Task 5)
|
||||
// - Use sessionStorage to pass credentials to "Try it" feature
|
||||
// Layout and render style for compact operation display
|
||||
element.setAttribute('layout', 'column');
|
||||
element.setAttribute('render-style', 'read');
|
||||
element.setAttribute('show-header', 'false');
|
||||
|
|
@ -270,10 +267,8 @@ function createRapiDocElement(
|
|||
element.setAttribute('use-path-in-nav-bar', 'false');
|
||||
element.setAttribute('show-info', 'false');
|
||||
|
||||
// Authentication display - hide RapiDoc's built-in auth section
|
||||
// We use a custom popover component for credential input instead
|
||||
// Credentials are applied via HTML attributes (api-key-name, api-key-value)
|
||||
// and the setApiKey() JavaScript API
|
||||
// Authentication display - disabled because RapiDoc's auth UI doesn't work
|
||||
// with match-paths filtering. We show a separate auth info banner instead.
|
||||
element.setAttribute('allow-authentication', 'false');
|
||||
element.setAttribute('show-components', 'false');
|
||||
|
||||
|
|
@ -319,84 +314,63 @@ function injectShadowStyles(element: HTMLElement): void {
|
|||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
/* Hide RapiDoc's built-in security section - we show our own */
|
||||
/* Target the authorization requirements shown near each operation */
|
||||
.api-key,
|
||||
.api-key-info,
|
||||
.security-info-button,
|
||||
[class*="api-key"],
|
||||
[class*="security-info"],
|
||||
.m-markdown-small:has(.lock-icon),
|
||||
div:has(> .lock-icon),
|
||||
/* Target the section showing "AUTHORIZATIONS:" or similar */
|
||||
.req-resp-container > div:first-child:has(svg[style*="lock"]),
|
||||
/* Target lock icons and their parent containers */
|
||||
svg.lock-icon,
|
||||
.lock-icon,
|
||||
/* Wide selectors for security-related elements */
|
||||
[part="section-operation-security"],
|
||||
.expanded-endpoint-body > div:first-child:has([class*="lock"]) {
|
||||
display: none !important;
|
||||
/* Fix text cutoff - ensure content flows responsively */
|
||||
.req-res-title,
|
||||
.resp-head,
|
||||
.api-request,
|
||||
.api-response,
|
||||
.param-name,
|
||||
.param-type,
|
||||
.descr {
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Allow wrapping in tables and flex containers */
|
||||
table {
|
||||
table-layout: auto !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
td, th {
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
/* Prevent horizontal overflow */
|
||||
.section-gap,
|
||||
.expanded-req-resp-container {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Fix Copy button overlapping code content in Try It section */
|
||||
/* The Copy button is absolutely positioned, so add padding to prevent overlap */
|
||||
.curl-request pre,
|
||||
.curl-request code,
|
||||
.request-body-container pre,
|
||||
.response-panel pre {
|
||||
padding-right: 4.5rem !important; /* Space for Copy button */
|
||||
}
|
||||
|
||||
/* Ensure Copy button stays visible and accessible */
|
||||
.copy-btn,
|
||||
button[title="Copy"] {
|
||||
z-index: 1;
|
||||
background: rgba(0, 163, 255, 0.9) !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Make code blocks wrap long URLs instead of requiring horizontal scroll */
|
||||
.curl-request code,
|
||||
.request-body-container code {
|
||||
white-space: pre-wrap !important;
|
||||
word-break: break-all;
|
||||
}
|
||||
`;
|
||||
shadowRoot.appendChild(style);
|
||||
|
||||
// Hide security badge elements by examining content
|
||||
const hideSecurityBadge = () => {
|
||||
// Find elements containing security-related text and hide their container
|
||||
const allElements = shadowRoot.querySelectorAll('span, div');
|
||||
allElements.forEach((el) => {
|
||||
const text = el.textContent?.trim();
|
||||
// Find leaf elements that contain authorization-related text
|
||||
if (
|
||||
el.children.length === 0 &&
|
||||
(text === 'HTTP Bearer' ||
|
||||
text === 'Bearer' ||
|
||||
text === 'AUTHORIZATIONS:' ||
|
||||
text === 'Authorization' ||
|
||||
text === 'api_token' ||
|
||||
text === 'BearerAuthentication')
|
||||
) {
|
||||
// Walk up the DOM to find a suitable container to hide
|
||||
// This hides both the text AND any sibling icons (like lock)
|
||||
let target: HTMLElement = el as HTMLElement;
|
||||
let parent: HTMLElement | null = el.parentElement;
|
||||
let depth = 0;
|
||||
while (parent && depth < 4) {
|
||||
// Stop at reasonable container boundaries
|
||||
if (
|
||||
parent.classList.contains('expanded-endpoint-body') ||
|
||||
parent.classList.contains('req-resp-container') ||
|
||||
parent.tagName === 'SECTION'
|
||||
) {
|
||||
break;
|
||||
}
|
||||
target = parent;
|
||||
parent = parent.parentElement;
|
||||
depth++;
|
||||
}
|
||||
target.style.display = 'none';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Run immediately and after delays for dynamic content
|
||||
hideSecurityBadge();
|
||||
setTimeout(hideSecurityBadge, 300);
|
||||
setTimeout(hideSecurityBadge, 800);
|
||||
|
||||
// Watch for dynamically added security elements
|
||||
const observer = new MutationObserver(() => {
|
||||
hideSecurityBadge();
|
||||
});
|
||||
observer.observe(shadowRoot, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
// Disconnect after 5 seconds to avoid performance issues
|
||||
setTimeout(() => observer.disconnect(), 5000);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -72,92 +72,61 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Dark theme overrides for security schemes
|
||||
[data-theme="dark"],
|
||||
html:has(link[title="dark-theme"]:not([disabled])) {
|
||||
.api-security-schemes {
|
||||
border-top-color: $grey25;
|
||||
|
||||
.security-scheme {
|
||||
background: $grey15;
|
||||
border-color: $grey25;
|
||||
}
|
||||
|
||||
.scheme-details {
|
||||
dt {
|
||||
color: $g15-platinum;
|
||||
}
|
||||
}
|
||||
|
||||
.scheme-description {
|
||||
border-top-color: $grey25;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// API Auth Popover - Credential input for operation pages
|
||||
// API Auth Modal - Credential input for operation pages
|
||||
//
|
||||
// Popover-based UI triggered by "Set credentials" button.
|
||||
// Positioned above RapiDoc, integrates with "Try it" via JavaScript API.
|
||||
// Modal UI triggered by "Set credentials" button in auth info banner.
|
||||
// Integrates with RapiDoc "Try it" via JavaScript API.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
.api-auth-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.api-auth-trigger-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.api-auth-schemes {
|
||||
font-size: 0.85rem;
|
||||
color: $g9-mountain;
|
||||
|
||||
.api-auth-label {
|
||||
font-weight: 400;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.api-auth-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: $article-text;
|
||||
background: $g20-white;
|
||||
border: 1px solid $g5-pepper;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba($b-pool, 0.08);
|
||||
border-color: $b-pool;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid $b-pool;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&.has-credentials {
|
||||
border-color: $gr-viridian;
|
||||
background: rgba($gr-viridian, 0.08);
|
||||
}
|
||||
|
||||
.auth-icon {
|
||||
color: $g9-mountain;
|
||||
}
|
||||
|
||||
&.has-credentials .auth-icon {
|
||||
color: $gr-viridian;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-status-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: $gr-viridian;
|
||||
border-radius: 50%;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.api-auth-popover {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
left: 0;
|
||||
// Backdrop overlay
|
||||
.api-auth-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 1000;
|
||||
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Credentials modal
|
||||
.api-auth-popover {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1001;
|
||||
min-width: 320px;
|
||||
max-width: 400px;
|
||||
background: $g20-white;
|
||||
border: 1px solid $g5-pepper;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
|
||||
|
||||
&[hidden] {
|
||||
display: none;
|
||||
|
|
@ -219,10 +188,6 @@
|
|||
color: $article-text;
|
||||
}
|
||||
|
||||
.auth-label-text {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.auth-label-hint {
|
||||
font-weight: 400;
|
||||
color: $g9-mountain;
|
||||
|
|
@ -284,25 +249,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.auth-field-group {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid $g5-pepper;
|
||||
|
||||
.auth-group-label {
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
color: $article-text;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
|
||||
// Explicit button styling to avoid link-like appearance
|
||||
.auth-apply,
|
||||
.auth-clear {
|
||||
padding: 0.4rem 0.75rem;
|
||||
|
|
@ -354,56 +305,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Dark theme overrides
|
||||
// Dark theme for modal
|
||||
[data-theme="dark"],
|
||||
html:has(link[title="dark-theme"]:not([disabled])) {
|
||||
.api-auth-schemes {
|
||||
color: $g15-platinum;
|
||||
}
|
||||
|
||||
.api-security-schemes {
|
||||
border-top-color: $grey25;
|
||||
|
||||
.security-scheme {
|
||||
background: $grey15;
|
||||
border-color: $grey25;
|
||||
}
|
||||
|
||||
.scheme-details {
|
||||
dt {
|
||||
color: $g15-platinum;
|
||||
}
|
||||
}
|
||||
|
||||
.scheme-description {
|
||||
border-top-color: $grey25;
|
||||
}
|
||||
}
|
||||
|
||||
.api-auth-trigger {
|
||||
background: $grey15;
|
||||
border-color: $grey25;
|
||||
color: $g20-white;
|
||||
|
||||
&:hover {
|
||||
background: rgba($b-pool, 0.1);
|
||||
border-color: $b-pool;
|
||||
}
|
||||
|
||||
&.has-credentials {
|
||||
border-color: $gr-viridian;
|
||||
background: rgba($gr-viridian, 0.1);
|
||||
}
|
||||
|
||||
.auth-icon {
|
||||
color: $g15-platinum;
|
||||
}
|
||||
|
||||
&.has-credentials .auth-icon {
|
||||
color: $gr-emerald;
|
||||
}
|
||||
}
|
||||
|
||||
.api-auth-popover {
|
||||
background: $grey15;
|
||||
border-color: $grey25;
|
||||
|
|
@ -459,14 +363,6 @@ html:has(link[title="dark-theme"]:not([disabled])) {
|
|||
}
|
||||
}
|
||||
|
||||
.auth-field-group {
|
||||
border-top-color: $grey25;
|
||||
|
||||
.auth-group-label {
|
||||
color: $g20-white;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-feedback {
|
||||
&.auth-feedback--success {
|
||||
background: rgba($gr-viridian, 0.15);
|
||||
|
|
@ -480,19 +376,7 @@ html:has(link[title="dark-theme"]:not([disabled])) {
|
|||
}
|
||||
|
||||
.auth-actions {
|
||||
.auth-apply {
|
||||
background: $b-pool;
|
||||
color: $g20-white;
|
||||
border-color: $b-pool;
|
||||
|
||||
&:hover {
|
||||
background: lighten($b-pool, 5%);
|
||||
border-color: lighten($b-pool, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
.auth-clear {
|
||||
background: transparent;
|
||||
color: $g15-platinum;
|
||||
border-color: $grey25;
|
||||
|
||||
|
|
|
|||
|
|
@ -41,41 +41,21 @@
|
|||
<link rel="alternate" type="application/json" href="{{ $specPathJSON | absURL }}" title="OpenAPI Specification (JSON)" />
|
||||
{{ end }}
|
||||
|
||||
{{/* Auth credentials popover trigger with scheme indicator */}}
|
||||
{{/* Map scheme codes to display names */}}
|
||||
{{ $schemeNames := dict "bearer" "HTTP Bearer" "token" "API Token" "basic" "HTTP Basic" "querystring" "Query String" }}
|
||||
{{ $schemeList := split $authSchemes "," }}
|
||||
{{ $displaySchemes := slice }}
|
||||
{{ range $schemeList }}
|
||||
{{ $name := index $schemeNames . }}
|
||||
{{ if $name }}
|
||||
{{ $displaySchemes = $displaySchemes | append $name }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{/* Auth credentials modal */}}
|
||||
<div class="api-auth-backdrop" hidden></div>
|
||||
<div class="api-auth-popover" role="dialog" aria-label="API credentials" hidden>
|
||||
{{/* Content rendered by TypeScript component */}}
|
||||
</div>
|
||||
|
||||
<div class="api-auth-row">
|
||||
<div class="api-auth-trigger-wrapper">
|
||||
<button type="button"
|
||||
class="api-auth-trigger btn btn-sm"
|
||||
data-component="api-auth-input"
|
||||
data-schemes="{{ $authSchemes }}"
|
||||
data-popover="true"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog">
|
||||
<svg class="auth-icon" width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
||||
<path d="M8 1a3.5 3.5 0 0 0-3.5 3.5V6H3a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1h-1.5V4.5A3.5 3.5 0 0 0 8 1zm2 5H6V4.5a2 2 0 1 1 4 0V6z"/>
|
||||
</svg>
|
||||
<span class="auth-trigger-text">Set credentials</span>
|
||||
<span class="auth-status-indicator" hidden aria-label="Credentials configured"></span>
|
||||
</button>
|
||||
{{/* Popover container - positioned by CSS */}}
|
||||
<div class="api-auth-popover" role="dialog" aria-label="API credentials" hidden>
|
||||
{{/* Content rendered by TypeScript component */}}
|
||||
</div>
|
||||
</div>
|
||||
<span class="api-auth-schemes" title="Supported authentication methods">
|
||||
<span class="api-auth-label">Auth:</span> {{ delimit $displaySchemes " or " }}
|
||||
</span>
|
||||
{{/* Authentication info banner with trigger for credentials modal */}}
|
||||
<div class="api-auth-info"
|
||||
data-component="api-auth-input"
|
||||
data-schemes="{{ $authSchemes }}">
|
||||
<svg class="api-auth-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
||||
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/>
|
||||
</svg>
|
||||
<span class="api-auth-status">This endpoint requires authentication.</span>
|
||||
<button type="button" class="api-auth-trigger">Set credentials</button>
|
||||
</div>
|
||||
|
||||
{{/* Component container - TypeScript handles initialization */}}
|
||||
|
|
@ -155,15 +135,57 @@ rapi-doc::part(section-tag) {
|
|||
display: none;
|
||||
}
|
||||
|
||||
/* Fix auth schemes at narrow widths - ensure content is scrollable */
|
||||
@media (max-width: 1280px) {
|
||||
.api-reference-mini {
|
||||
overflow-x: auto;
|
||||
}
|
||||
/* Responsive layout - allow content to flow naturally */
|
||||
.api-reference-mini {
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
rapi-doc-mini, rapi-doc {
|
||||
min-width: 800px; /* Prevent auth elements from collapsing/overlapping */
|
||||
}
|
||||
/* Authentication info banner */
|
||||
.api-auth-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
background: #F0F4FF;
|
||||
border: 1px solid rgba(0, 163, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
color: #545667;
|
||||
}
|
||||
|
||||
.api-auth-info .api-auth-icon {
|
||||
flex-shrink: 0;
|
||||
color: #00A3FF;
|
||||
}
|
||||
|
||||
.api-auth-info .api-auth-status {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.api-auth-info .api-auth-trigger {
|
||||
background: none;
|
||||
border: 1px solid #00A3FF;
|
||||
color: #00A3FF;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.api-auth-info .api-auth-trigger:hover {
|
||||
background: #00A3FF;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Dark mode */
|
||||
[data-theme="dark"] .api-auth-info,
|
||||
html:has(link[title="dark-theme"]:not([disabled])) .api-auth-info {
|
||||
background: #1a1a2e;
|
||||
border-color: rgba(0, 163, 255, 0.2);
|
||||
color: #D4D7DD;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue