- 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>
278 lines
8.9 KiB
TypeScript
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>
|
|
);
|
|
}
|