2026-01-18 12:51:34 +00:00
|
|
|
import { useEffect, useRef, useState } from 'react';
|
2026-01-18 12:50:47 +00:00
|
|
|
import SwaggerUI from 'swagger-ui-react';
|
|
|
|
|
import 'swagger-ui-react/swagger-ui.css';
|
|
|
|
|
import { env } from '@/config/env';
|
|
|
|
|
import { logger } from '@/utils/logger';
|
2026-01-18 12:51:34 +00:00
|
|
|
import { AlertCircle, RefreshCw } from 'lucide-react';
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
2026-01-18 12:50:47 +00:00
|
|
|
|
|
|
|
|
interface SwaggerUIProps {
|
|
|
|
|
specUrl?: string;
|
|
|
|
|
spec?: object;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Composant Swagger UI pour afficher la documentation API
|
|
|
|
|
* Charge le fichier OpenAPI depuis le backend ou utilise un spec fourni
|
|
|
|
|
*/
|
|
|
|
|
export function SwaggerUIDoc({ specUrl, spec }: SwaggerUIProps) {
|
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
2026-01-18 12:51:34 +00:00
|
|
|
const [error, setError] = useState<string | null>(null);
|
2026-01-18 12:50:47 +00:00
|
|
|
|
|
|
|
|
// Construire l'URL du fichier OpenAPI/Swagger
|
|
|
|
|
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}`;
|
|
|
|
|
|
2026-01-18 12:54:32 +00:00
|
|
|
// Le backend sert Swagger via gin-swagger
|
|
|
|
|
// gin-swagger sert généralement à /swagger/index.html mais le JSON peut être à différents endroits
|
2026-01-18 12:50:47 +00:00
|
|
|
const baseUrl = apiBase.replace(/\/api\/v1$/, '');
|
2026-01-18 12:54:32 +00:00
|
|
|
|
|
|
|
|
// Essayer plusieurs endpoints possibles pour le JSON Swagger
|
|
|
|
|
// gin-swagger utilise généralement /swagger/doc.json mais peut aussi servir via /swagger/index.html
|
|
|
|
|
// Si le backend sert le fichier statique, utiliser /docs/swagger.json
|
|
|
|
|
// Pour l'instant, utiliser le fichier statique qui existe dans le repo
|
|
|
|
|
// Si ça ne fonctionne pas, l'utilisateur peut utiliser le bouton "Open in New Tab" pour accéder à /swagger/index.html
|
|
|
|
|
return `${baseUrl}/docs/swagger.json`;
|
2026-01-18 12:50:47 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (containerRef.current) {
|
|
|
|
|
logger.debug('Swagger UI initialized', {
|
|
|
|
|
specUrl: specUrl || getOpenApiUrl(),
|
|
|
|
|
hasSpec: !!spec,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, [specUrl, spec]);
|
|
|
|
|
|
|
|
|
|
const swaggerConfig = {
|
|
|
|
|
url: spec ? undefined : getOpenApiUrl(),
|
|
|
|
|
spec: spec,
|
|
|
|
|
deepLinking: true,
|
|
|
|
|
displayOperationId: false,
|
|
|
|
|
defaultModelsExpandDepth: 1,
|
|
|
|
|
defaultModelExpandDepth: 1,
|
|
|
|
|
docExpansion: 'list' as const,
|
|
|
|
|
filter: true,
|
|
|
|
|
showExtensions: true,
|
|
|
|
|
showCommonExtensions: true,
|
|
|
|
|
tryItOutEnabled: true,
|
2026-01-18 12:51:34 +00:00
|
|
|
supportedSubmitMethods: ['get', 'post', 'put', 'delete', 'patch'] as ('get' | 'post' | 'put' | 'delete' | 'patch')[],
|
2026-01-18 12:50:47 +00:00
|
|
|
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: () => {
|
2026-01-18 12:51:34 +00:00
|
|
|
setError(null);
|
2026-01-18 12:50:47 +00:00
|
|
|
logger.debug('Swagger UI loaded successfully', {
|
|
|
|
|
url: getOpenApiUrl(),
|
|
|
|
|
});
|
|
|
|
|
},
|
2026-01-18 12:51:34 +00:00
|
|
|
onFailure: (err: Error) => {
|
|
|
|
|
setError(err.message || 'Failed to load Swagger documentation');
|
2026-01-18 12:50:47 +00:00
|
|
|
logger.error('Failed to load Swagger UI', {
|
2026-01-18 12:51:34 +00:00
|
|
|
error: err.message,
|
|
|
|
|
stack: err.stack,
|
2026-01-18 12:50:47 +00:00
|
|
|
url: getOpenApiUrl(),
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-18 12:51:34 +00:00
|
|
|
const handleRetry = () => {
|
|
|
|
|
setError(null);
|
|
|
|
|
// Force reload by updating the key
|
|
|
|
|
window.location.reload();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col items-center justify-center p-12 min-h-[600px]">
|
|
|
|
|
<AlertCircle className="w-16 h-16 text-kodo-red mb-4" />
|
|
|
|
|
<h3 className="text-xl font-bold text-white mb-2">
|
|
|
|
|
Failed to Load API Documentation
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-sm text-kodo-secondary mb-4 text-center max-w-md">
|
|
|
|
|
{error}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-xs text-kodo-content-dim mb-6 text-center max-w-md">
|
|
|
|
|
Trying to load from: {getOpenApiUrl()}
|
|
|
|
|
</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(getOpenApiUrl(), '_blank')}
|
|
|
|
|
variant="outline"
|
|
|
|
|
>
|
|
|
|
|
Open in New Tab
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:50:47 +00:00
|
|
|
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: #66fcf1;
|
|
|
|
|
}
|
|
|
|
|
.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: #66fcf1;
|
|
|
|
|
color: #000;
|
|
|
|
|
border: none;
|
|
|
|
|
}
|
|
|
|
|
.swagger-ui-container .swagger-ui .btn:hover {
|
|
|
|
|
background: #52e8e0;
|
|
|
|
|
}
|
|
|
|
|
.swagger-ui-container .swagger-ui .btn.execute {
|
|
|
|
|
background: #66fcf1;
|
|
|
|
|
color: #000;
|
|
|
|
|
}
|
|
|
|
|
.swagger-ui-container .swagger-ui .btn.cancel {
|
|
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
|
|
color: #fff;
|
|
|
|
|
}
|
|
|
|
|
`}</style>
|
|
|
|
|
<SwaggerUI {...swaggerConfig} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|