Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"@rjsf/shadcn": "6.5.1",
"@rjsf/utils": "6.5.1",
"@rjsf/validator-ajv8": "6.5.1",
"@scalar/api-reference-react": "0.6.31",
"@tanstack/react-table": "8.21.3",
"@types/node": "24.1.0",
"@xterm/addon-fit": "0.11.0",
Expand Down
1,704 changes: 1,695 additions & 9 deletions ui/pnpm-lock.yaml

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
import { UserRole } from './api/v1/schema';
import AdministrationPage from './pages/administration';
import APIKeysPage from './pages/api-keys';
import APIDocsPage from './pages/api-docs';
import AuditLogsPage from './pages/audit-logs';
import BaseConfigPage from './pages/base-config';
import DAGRuns from './pages/dag-runs';
Expand Down Expand Up @@ -531,6 +532,10 @@ function AppInner({ config: initialConfig }: Props): React.ReactElement {
path="/home"
element={<HomePage />}
/>
<Route
path="/api-docs"
element={<APIDocsPage />}
/>
<Route
path="/integrations"
element={<IntegrationsPage />}
Expand Down
1 change: 1 addition & 0 deletions ui/src/__tests__/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ vi.mock('../pages/administration', () => ({
default: () => <h1>Administration</h1>,
}));
vi.mock('../pages/api-keys', () => ({ default: () => <h1>API Keys</h1> }));
vi.mock('../pages/api-docs', () => ({ default: () => <h1>API Docs</h1> }));
vi.mock('../pages/audit-logs', () => ({
default: () => <h1>Audit Logs</h1>,
}));
Expand Down
4 changes: 4 additions & 0 deletions ui/src/__tests__/menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,9 @@ describe('sidebar menu', () => {
screen.getByRole('button', { name: 'Toggle Administration section' })
).toHaveAttribute('aria-expanded', 'false');

expect(
screen.getByRole('link', { name: 'API Reference' })
).not.toBeVisible();
expect(
screen.queryByRole('link', { name: 'Dashboard' })
).not.toBeInTheDocument();
Expand Down Expand Up @@ -336,6 +339,7 @@ describe('sidebar menu', () => {
);
const integrationSubmenuItems = [
screen.getByRole('link', { name: 'Webhooks' }),
screen.getByRole('link', { name: 'API Reference' }),
];
for (const item of integrationSubmenuItems) {
expect(item).toBeVisible();
Expand Down
3 changes: 2 additions & 1 deletion ui/src/layouts/ContentNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const STATIC_ROUTE_LABELS: Record<string, string> = {
'/home': 'Home',
'/dashboard': 'Timeline',
'/cockpit': 'Cockpit',
'/api-docs': 'API Reference',
'/integrations': 'Integrations',
'/notifications': 'Notifications',
'/notification-rules': 'Notification Rules',
Expand Down Expand Up @@ -143,7 +144,7 @@ export function getBreadcrumbItems(pathname: string): BreadcrumbItemData[] {
return items;
}

if (['integrations', 'webhooks'].includes(segments[0] ?? '')) {
if (['integrations', 'webhooks', 'api-docs'].includes(segments[0] ?? '')) {
if (normalized !== '/integrations') {
items.push({ label: 'Integrations', to: '/integrations' });
}
Expand Down
9 changes: 8 additions & 1 deletion ui/src/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -799,7 +799,7 @@ export const mainListItems = React.forwardRef<
icon={<Webhook size={18} />}
label="Integrations"
isOpen={isOpen}
basePath={['/integrations', '/webhooks']}
basePath={['/integrations', '/webhooks', '/api-docs']}
to="/integrations"
onClick={onNavItemClick}
customColor={customColor}
Expand All @@ -813,6 +813,13 @@ export const mainListItems = React.forwardRef<
customColor={customColor}
/>
)}
<NavItem
to="/api-docs"
text="API Reference"
isOpen={isOpen}
onClick={onNavItemClick}
customColor={customColor}
/>
</NavGroup>

{canManageProfiles && (
Expand Down
66 changes: 66 additions & 0 deletions ui/src/pages/api-docs/ScalarViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (C) 2026 Yota Hamada
// SPDX-License-Identifier: GPL-3.0-or-later

import { ApiReferenceReact } from '@scalar/api-reference-react';
import '@scalar/api-reference-react/style.css';
import * as React from 'react';

type ScalarViewerProps = {
spec: Record<string, unknown>;
preferredBearerToken?: string;
};

export default function ScalarViewer({
spec,
preferredBearerToken,
}: ScalarViewerProps): React.ReactElement {
const [darkMode, setDarkMode] = React.useState(
() =>
typeof document !== 'undefined' &&
document.documentElement.classList.contains('dark')
);

React.useEffect(() => {
if (typeof document === 'undefined') {
return;
}

const updateDarkMode = () => {
setDarkMode(document.documentElement.classList.contains('dark'));
};
updateDarkMode();

const observer = new MutationObserver(updateDarkMode);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class'],
});

return () => observer.disconnect();
}, []);

const configuration: Record<string, unknown> = {
content: spec,
layout: 'modern',
hideDarkModeToggle: true,
withDefaultFonts: false,
forceDarkModeState: darkMode ? 'dark' : 'light',
};

if (preferredBearerToken) {
configuration.authentication = {
preferredSecurityScheme: 'apiToken',
securitySchemes: {
apiToken: {
token: preferredBearerToken,
},
},
};
}

return (
<div className="api-docs-viewer h-full min-h-0">
<ApiReferenceReact configuration={configuration} />
</div>
);
}
Loading