veza/apps/web/src/components/developer/SwaggerUI.tsx
senke 73e8372b0e refactor: Phase 7 — Clean up legacy components and remove dead tokens
- Bulk replace text-white → text-foreground across 116 component files
  (preserving text-white/ opacity variants)
- Remove hover-glow-cyan, shadow-card-glow-cyan, shadow-button-primary-glow
  classes from all components
- Replace --duration-normal/--duration-immersive/--duration-slow with
  --sumi-duration-normal/--sumi-duration-slow across 130+ files
- Replace --ease-out/--ease-in-out with --sumi-ease-out/--sumi-ease-in-out
- Replace focus:ring-blue-500 → focus:ring-primary (4 files)
- Remove hover:scale-105/110 and hover:-translate-y-1/0.5 transforms
  (SUMI anti-pattern: no scale on hover)
- Clean up stale kodo- references in comments

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 02:09:29 +01:00

278 lines
8.9 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import SwaggerUI from 'swagger-ui-react';
import 'swagger-ui-react/swagger-ui.css';
import { env } from '@/config/env';
import { logger } from '@/utils/logger';
import { AlertCircle, RefreshCw } from 'lucide-react';
import { Button } from '@/components/ui/button';
interface SwaggerUIProps {
specUrl?: string;
spec?: object;
useIframe?: boolean; // Option pour utiliser un iframe au lieu du composant React
}
/**
* Composant Swagger UI pour afficher la documentation API
* Charge le fichier OpenAPI depuis le backend ou utilise un spec fourni
* Peut utiliser un iframe pour charger Swagger UI HTML directement
*/
export function SwaggerUIDoc({ specUrl, spec, useIframe = false }: SwaggerUIProps) {
const containerRef = useRef<HTMLDivElement>(null);
const iframeRef = useRef<HTMLIFrameElement>(null);
const [error, setError] = useState<string | null>(null);
// Construire l'URL du fichier OpenAPI/Swagger ou Swagger UI HTML
const getOpenApiUrl = () => {
if (specUrl) return specUrl;
// Si API_URL est relatif, construire l'URL complète
const apiBase = env.API_URL.startsWith('http')
? env.API_URL
: `${window.location.origin}${env.API_URL}`;
const baseUrl = apiBase.replace(/\/api\/v1$/, '');
// Si useIframe est true, retourner l'URL de Swagger UI HTML
if (useIframe) {
return `${baseUrl}/swagger/index.html`;
}
// Sinon, essayer de charger le JSON Swagger
// gin-swagger peut servir le JSON à différents endroits
// Essayer /swagger/doc.json (endpoint standard gin-swagger)
return `${baseUrl}/swagger/doc.json`;
};
const getSwaggerUIUrl = () => {
const apiBase = env.API_URL.startsWith('http')
? env.API_URL
: `${window.location.origin}${env.API_URL}`;
const baseUrl = apiBase.replace(/\/api\/v1$/, '');
return `${baseUrl}/swagger/index.html`;
};
useEffect(() => {
if (containerRef.current) {
logger.debug('Swagger UI initialized', {
specUrl: specUrl || getOpenApiUrl(),
hasSpec: !!spec,
useIframe,
});
}
}, [specUrl, spec, useIframe]);
const swaggerConfig = {
url: spec ? undefined : getOpenApiUrl(),
spec,
deepLinking: true,
displayOperationId: false,
defaultModelsExpandDepth: 1,
defaultModelExpandDepth: 1,
docExpansion: 'list' as const,
filter: true,
showExtensions: true,
showCommonExtensions: true,
tryItOutEnabled: true,
supportedSubmitMethods: ['get', 'post', 'put', 'delete', 'patch'] as ('get' | 'post' | 'put' | 'delete' | 'patch')[],
requestInterceptor: (request: any) => {
// Ajouter le token d'authentification si disponible
const token = localStorage.getItem('access_token');
if (token && request.headers) {
request.headers['Authorization'] = `Bearer ${token}`;
}
// Ajouter le CSRF token si disponible
const csrfToken = localStorage.getItem('csrf_token');
if (csrfToken && request.headers) {
request.headers['X-CSRF-Token'] = csrfToken;
}
return request;
},
onComplete: () => {
setError(null);
logger.debug('Swagger UI loaded successfully', {
url: getOpenApiUrl(),
});
},
onFailure: (err: Error) => {
setError(err.message || 'Failed to load Swagger documentation');
// YAMLException when receiving HTML = wrong server or proxy misconfigured
const isHtmlInsteadOfJson = err.message?.includes('end of the stream') && /<(!DOCTYPE|!--|html)/i.test(err.message);
const logLevel = isHtmlInsteadOfJson ? 'debug' : 'error';
logger[logLevel]('Failed to load Swagger UI', {
error: err.message,
stack: err.stack,
url: getOpenApiUrl(),
});
},
};
const handleRetry = () => {
setError(null);
// Force reload by updating the key
window.location.reload();
};
// Si useIframe est true, utiliser un iframe pour charger Swagger UI HTML directement
if (useIframe) {
return (
<div
ref={containerRef}
className="swagger-ui-container"
style={{
height: '100%',
minHeight: '600px',
}}
>
<iframe
ref={iframeRef}
src={getSwaggerUIUrl()}
className="w-full h-full border-0 rounded-lg"
style={{ minHeight: '600px' }}
title="Swagger UI Documentation"
onLoad={() => {
logger.debug('Swagger UI iframe loaded successfully');
setError(null);
}}
onError={() => {
setError('Failed to load Swagger UI in iframe');
logger.error('Failed to load Swagger UI iframe');
}}
/>
</div>
);
}
if (error) {
const swaggerUiUrl = getSwaggerUIUrl();
return (
<div className="flex flex-col items-center justify-center p-12 min-h-layout-page">
<AlertCircle className="w-16 h-16 text-destructive mb-4" />
<h3 className="text-xl font-bold text-foreground mb-2">
Failed to Load API Documentation
</h3>
<p className="text-sm text-muted-foreground mb-4 text-center max-w-md">
{error}
</p>
<p className="text-xs text-muted-foreground mb-6 text-center max-w-md">
Trying to load from: {getOpenApiUrl()}
<br />
<span className="text-muted-foreground">
The Swagger JSON endpoint may not be available. Try opening Swagger UI directly.
</span>
</p>
<div className="flex gap-4">
<Button onClick={handleRetry} variant="default">
<RefreshCw className="w-4 h-4 mr-2" />
Retry
</Button>
<Button
onClick={() => window.open(swaggerUiUrl, '_blank')}
variant="outline"
>
Open Swagger UI
</Button>
<Button
onClick={() => {
// Basculer vers iframe mode
window.location.reload();
}}
variant="outline"
>
Use Iframe Mode
</Button>
</div>
</div>
);
}
return (
<div
ref={containerRef}
className="swagger-ui-container"
style={{
height: '100%',
minHeight: '600px',
}}
>
<style>{`
.swagger-ui-container .swagger-ui {
background: transparent;
}
.swagger-ui-container .swagger-ui .topbar {
display: none;
}
.swagger-ui-container .swagger-ui .info {
margin: 20px 0;
}
.swagger-ui-container .swagger-ui .info .title {
color: #fff;
}
.swagger-ui-container .swagger-ui .scheme-container {
background: rgba(255, 255, 255, 0.05);
padding: 10px;
border-radius: 8px;
margin: 20px 0;
}
.swagger-ui-container .swagger-ui .opblock {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
margin: 10px 0;
}
.swagger-ui-container .swagger-ui .opblock.opblock-post {
border-color: rgba(102, 252, 241, 0.3);
}
.swagger-ui-container .swagger-ui .opblock.opblock-get {
border-color: rgba(102, 252, 241, 0.3);
}
.swagger-ui-container .swagger-ui .opblock.opblock-put {
border-color: rgba(255, 193, 7, 0.3);
}
.swagger-ui-container .swagger-ui .opblock.opblock-delete {
border-color: rgba(244, 67, 54, 0.3);
}
.swagger-ui-container .swagger-ui .opblock .opblock-summary {
color: #fff;
}
.swagger-ui-container .swagger-ui .opblock .opblock-description-wrapper {
color: rgba(255, 255, 255, 0.8);
}
.swagger-ui-container .swagger-ui .parameter__name {
color: #7c9dd6;
}
.swagger-ui-container .swagger-ui .response-col_status {
color: #fff;
}
.swagger-ui-container .swagger-ui .response-col_description {
color: rgba(255, 255, 255, 0.8);
}
.swagger-ui-container .swagger-ui input,
.swagger-ui-container .swagger-ui select,
.swagger-ui-container .swagger-ui textarea {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
}
.swagger-ui-container .swagger-ui .btn {
background: #7c9dd6;
color: #000;
border: none;
}
.swagger-ui-container .swagger-ui .btn:hover {
background: #93afe0;
}
.swagger-ui-container .swagger-ui .btn.execute {
background: #7c9dd6;
color: #000;
}
.swagger-ui-container .swagger-ui .btn.cancel {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
`}</style>
<SwaggerUI {...swaggerConfig} />
</div>
);
}