diff --git a/frontend/src/pages/api-docs/ApiDocsPage.css b/frontend/src/pages/api-docs/ApiDocsPage.css index 6e5450f83..178f94795 100644 --- a/frontend/src/pages/api-docs/ApiDocsPage.css +++ b/frontend/src/pages/api-docs/ApiDocsPage.css @@ -45,15 +45,14 @@ } .api-docs-page .websocket-events { - margin-bottom: 16px; padding: 20px; background: var(--bg-card); border: 1px solid var(--ant-color-border-secondary); border-radius: 8px; } -.api-docs-page .websocket-events h2 { - margin-top: 0; +.api-docs-page .swagger-ui .section-tabs { + margin-top: 20px; } .api-docs-page .websocket-events pre { diff --git a/frontend/src/pages/api-docs/ApiDocsPage.tsx b/frontend/src/pages/api-docs/ApiDocsPage.tsx index 204e2c443..38e26e7d9 100644 --- a/frontend/src/pages/api-docs/ApiDocsPage.tsx +++ b/frontend/src/pages/api-docs/ApiDocsPage.tsx @@ -1,6 +1,6 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { Card, Col, ConfigProvider, Layout, Row, Typography } from 'antd'; +import { Card, Col, ConfigProvider, Layout, Row, Tabs, Typography } from 'antd'; import SwaggerUI from 'swagger-ui-react'; import 'swagger-ui-react/swagger-ui.css'; @@ -14,6 +14,61 @@ const basePath = window.X_UI_BASE_PATH || ''; const openApiUrl = `${basePath}panel/api/openapi.json`; const websocketEvents = buildWebSocketEvents(EXAMPLES); +interface TaggedOperations { + keySeq: () => { first: () => string | undefined }; + filter: (keep: (operations: unknown, tag: string) => boolean) => TaggedOperations; +} + +interface LayoutSelectors { + currentFilter: () => string | false; +} + +interface SectionTabsProps { + specSelectors: { tags: () => { toJS: () => { name: string }[] } }; + layoutSelectors: LayoutSelectors; + layoutActions: { updateFilter: (tag: string) => void }; +} + +function SectionTabs({ specSelectors, layoutSelectors, layoutActions }: SectionTabsProps) { + const tags = specSelectors + .tags() + .toJS() + .map((tag) => tag.name); + return ( +
+ ({ key: tag, label: tag }))} + /> +
+ ); +} + +// Shows one tag at a time, the first until a tab is picked. Swagger's own filter is a +// substring match ("Settings" would also show "Xray Settings") and no-op while unset. +const sectionTabsPlugin = { + statePlugins: { + spec: { + wrapSelectors: { + taggedOperations: + ( + select: (...args: unknown[]) => TaggedOperations, + system: { getSystem: () => { layoutSelectors: LayoutSelectors } }, + ) => + (...args: unknown[]) => { + const operations = select(...args); + const active = + system.getSystem().layoutSelectors.currentFilter() || operations.keySeq().first(); + return operations.filter((_, tag) => tag === active); + }, + }, + }, + }, + components: { FilterContainer: SectionTabs }, +}; + export default function ApiDocsPage() { const { isDark, isUltra, antdThemeConfig } = useTheme(); const { t } = useTranslation(); @@ -32,36 +87,54 @@ export default function ApiDocsPage() { -
- - WebSocket events - - - After the cookie-authenticated GET /ws{' '} - upgrade, every server message uses{' '} - {'{ type, payload, time }'}. The time value - is Unix milliseconds. - - - {websocketEvents.map((event) => ( - - {event.type}}> - {event.summary} -
{JSON.stringify(event.example, null, 2)}
-
- - ))} -
-
-
- -
+ + + + ), + }, + { + key: 'websocket-events', + label: 'WebSocket events', + children: ( +
+ + After the cookie-authenticated{' '} + GET /ws upgrade, every server + message uses{' '} + {'{ type, payload, time }'}. The + time value is Unix milliseconds. + + + {websocketEvents.map((event) => ( + + {event.type}} + > + {event.summary} +
{JSON.stringify(event.example, null, 2)}
+
+ + ))} +
+
+ ), + }, + ]} + />