veza/apps/web/src/components/developer/SwaggerUI.tsx

226 lines
7.2 KiB
TypeScript
Raw Normal View History

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;
}
/**
* 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);
const [error, setError] = useState<string | null>(null);
// Construire l'URL du fichier OpenAPI/Swagger avec fallback
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$/, '');
// gin-swagger peut servir le JSON à différents endroits
// Essayer /swagger/doc.json (endpoint standard gin-swagger)
// Si ça ne fonctionne pas, l'utilisateur peut utiliser le bouton "Open in New Tab" pour accéder à /swagger/index.html
return `${baseUrl}/swagger/index.html``;
};
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,
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');
logger.error('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();
};
if (error) {
const swaggerUiUrl = getOpenApiUrl().replace('/swagger/doc.json', '/swagger/index.html');
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()}
<br />
<span className="text-kodo-steel">
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={() => window.open(getOpenApiUrl(), '_blank')}
variant="outline"
>
Open JSON Directly
</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: #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>
);
}