feat: initial scaffold — MVP catalogue de données d'API
- Backend NestJS : CRUD api_entries + categories, upload YAML multi-fichiers, génération docs AsyncAPI (@asyncapi/cli@6.0.0) et OpenAPI (redocly) - Fix: route wildcard GET /api/apis/:id/*path pour servir les assets statiques (CSS/JS) générés par AsyncAPI HTML template (contournement bug path-to-regexp v8) - Frontend Angular 19 : pages catalog, browse, api-detail, doc-viewer, upload (DSFR) - Seeder de données de démo (idempotent) - Docker Compose dev + Dockerfiles + manifests K8s - Documentation : README, architecture, référence API REST Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
237
front-public/src/app/pages/api-detail/api-detail.component.ts
Normal file
237
front-public/src/app/pages/api-detail/api-detail.component.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import {
|
||||
Component,
|
||||
signal,
|
||||
inject,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
ChangeDetectionStrategy,
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { ApiService } from '../../core/api.service';
|
||||
import { ApiEntry } from '@datacat/shared';
|
||||
|
||||
@Component({
|
||||
selector: 'app-api-detail',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterLink],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="fr-container fr-mt-4w fr-mb-4w">
|
||||
<!-- Fil d'Ariane -->
|
||||
<nav class="fr-breadcrumb fr-mb-3w" aria-label="vous êtes ici">
|
||||
<ol class="fr-breadcrumb__list">
|
||||
<li>
|
||||
<a class="fr-breadcrumb__link" routerLink="/catalog">Catalogue</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="fr-breadcrumb__link" aria-current="page">
|
||||
{{ api()?.title ?? 'Chargement...' }}
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
@if (loading()) {
|
||||
<div class="fr-callout fr-mb-4w">
|
||||
<p class="fr-callout__text">Chargement...</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (error()) {
|
||||
<div class="fr-alert fr-alert--error fr-mb-4w">
|
||||
<p class="fr-alert__title">Erreur</p>
|
||||
<p>{{ error() }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (api(); as entry) {
|
||||
<!-- En-tête -->
|
||||
<div class="fr-grid-row fr-grid-row--middle fr-mb-3w">
|
||||
<div class="fr-col">
|
||||
<h1 class="fr-h2 fr-mb-1w">{{ entry.title }}</h1>
|
||||
<div class="fr-tags-group">
|
||||
<span class="fr-tag">{{ entry.type }}</span>
|
||||
<span class="fr-tag">v{{ entry.version }}</span>
|
||||
<span [class]="badgeClass(entry.status)">{{ entry.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alerte statut PENDING -->
|
||||
@if (entry.status === 'PENDING') {
|
||||
<div class="fr-alert fr-alert--info fr-mb-3w">
|
||||
<p class="fr-alert__title">Génération en cours</p>
|
||||
<p>La documentation est en cours de génération. Cette page se rafraîchit automatiquement.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Alerte statut ERROR -->
|
||||
@if (entry.status === 'ERROR') {
|
||||
<div class="fr-alert fr-alert--error fr-mb-3w">
|
||||
<p class="fr-alert__title">Erreur de génération</p>
|
||||
<p>{{ entry.errorMessage }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Métadonnées -->
|
||||
<div class="fr-card fr-mb-4w">
|
||||
<div class="fr-card__body">
|
||||
<div class="fr-card__content">
|
||||
<h2 class="fr-h5 fr-mb-2w">Informations</h2>
|
||||
<dl class="fr-grid-row fr-grid-row--gutters">
|
||||
<div class="fr-col-12 fr-col-md-4">
|
||||
<dt class="fr-text--bold">Titre</dt>
|
||||
<dd>{{ entry.title }}</dd>
|
||||
</div>
|
||||
<div class="fr-col-12 fr-col-md-4">
|
||||
<dt class="fr-text--bold">Version</dt>
|
||||
<dd>{{ entry.version }}</dd>
|
||||
</div>
|
||||
<div class="fr-col-12 fr-col-md-4">
|
||||
<dt class="fr-text--bold">Type</dt>
|
||||
<dd>{{ entry.type }}</dd>
|
||||
</div>
|
||||
@if (entry.description) {
|
||||
<div class="fr-col-12">
|
||||
<dt class="fr-text--bold">Description</dt>
|
||||
<dd>{{ entry.description }}</dd>
|
||||
</div>
|
||||
}
|
||||
<div class="fr-col-12 fr-col-md-6">
|
||||
<dt class="fr-text--bold">Créé le</dt>
|
||||
<dd>{{ entry.createdAt | date:'dd/MM/yyyy HH:mm' }}</dd>
|
||||
</div>
|
||||
<div class="fr-col-12 fr-col-md-6">
|
||||
<dt class="fr-text--bold">Mis à jour le</dt>
|
||||
<dd>{{ entry.updatedAt | date:'dd/MM/yyyy HH:mm' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="fr-btns-group fr-btns-group--inline-sm">
|
||||
<a
|
||||
[routerLink]="['/catalog', entry.id, 'docs']"
|
||||
class="fr-btn"
|
||||
[class.fr-btn--disabled]="entry.status !== 'GENERATED'"
|
||||
[attr.aria-disabled]="entry.status !== 'GENERATED' ? 'true' : null"
|
||||
>
|
||||
Voir la documentation
|
||||
</a>
|
||||
<a
|
||||
[href]="'/api/apis/' + entry.id + '/yaml'"
|
||||
class="fr-btn fr-btn--secondary"
|
||||
download
|
||||
>
|
||||
Télécharger YAML
|
||||
</a>
|
||||
<button
|
||||
class="fr-btn fr-btn--secondary"
|
||||
(click)="onRegenerate()"
|
||||
[disabled]="regenerating()"
|
||||
>
|
||||
{{ regenerating() ? 'Régénération...' : 'Régénérer' }}
|
||||
</button>
|
||||
<button
|
||||
class="fr-btn fr-btn--secondary"
|
||||
(click)="onDelete()"
|
||||
[disabled]="deleting()"
|
||||
>
|
||||
{{ deleting() ? 'Suppression...' : 'Supprimer' }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class ApiDetailComponent implements OnInit, OnDestroy {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private apiService = inject(ApiService);
|
||||
|
||||
api = signal<ApiEntry | null>(null);
|
||||
loading = signal(false);
|
||||
error = signal<string | null>(null);
|
||||
regenerating = signal(false);
|
||||
deleting = signal(false);
|
||||
|
||||
private pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||
private id = '';
|
||||
|
||||
ngOnInit() {
|
||||
this.id = this.route.snapshot.paramMap.get('id') ?? '';
|
||||
this.loadApi();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.clearPoll();
|
||||
}
|
||||
|
||||
onRegenerate() {
|
||||
this.regenerating.set(true);
|
||||
this.apiService.regenerate(this.id).subscribe({
|
||||
next: (entry) => {
|
||||
this.api.set(entry);
|
||||
this.regenerating.set(false);
|
||||
this.startPollIfPending();
|
||||
},
|
||||
error: () => this.regenerating.set(false),
|
||||
});
|
||||
}
|
||||
|
||||
onDelete() {
|
||||
if (!confirm('Supprimer cette API ?')) return;
|
||||
this.deleting.set(true);
|
||||
this.apiService.delete(this.id).subscribe({
|
||||
next: () => this.router.navigate(['/catalog']),
|
||||
error: () => this.deleting.set(false),
|
||||
});
|
||||
}
|
||||
|
||||
badgeClass(status: string): string {
|
||||
if (status === 'GENERATED') return 'fr-badge fr-badge--success';
|
||||
if (status === 'ERROR') return 'fr-badge fr-badge--error';
|
||||
return 'fr-badge fr-badge--info';
|
||||
}
|
||||
|
||||
private loadApi() {
|
||||
this.loading.set(true);
|
||||
this.apiService.get(this.id).subscribe({
|
||||
next: (entry) => {
|
||||
this.api.set(entry);
|
||||
this.loading.set(false);
|
||||
this.startPollIfPending();
|
||||
},
|
||||
error: (err) => {
|
||||
this.error.set(err.message ?? 'Erreur lors du chargement');
|
||||
this.loading.set(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private startPollIfPending() {
|
||||
this.clearPoll();
|
||||
if (this.api()?.status === 'PENDING') {
|
||||
this.pollInterval = setInterval(() => this.pollStatus(), 3000);
|
||||
}
|
||||
}
|
||||
|
||||
private pollStatus() {
|
||||
this.apiService.get(this.id).subscribe({
|
||||
next: (entry) => {
|
||||
this.api.set(entry);
|
||||
if (entry.status !== 'PENDING') this.clearPoll();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private clearPoll() {
|
||||
if (this.pollInterval) {
|
||||
clearInterval(this.pollInterval);
|
||||
this.pollInterval = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user