import { Component, OnInit, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { ArtifactService } from '../../services/artifact'; import { Artifact } from '../../models/artifact.model'; import { interval, Subscription } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { LucideAngularModule, RefreshCw, Search, X, Download, Trash2, Sparkles } from 'lucide-angular'; @Component({ selector: 'app-artifacts-list', standalone: true, imports: [CommonModule, FormsModule, LucideAngularModule], templateUrl: './artifacts-list.html', styleUrls: ['./artifacts-list.css'] }) export class ArtifactsListComponent implements OnInit, OnDestroy { artifacts: Artifact[] = []; filteredArtifacts: Artifact[] = []; selectedArtifact: Artifact | null = null; searchTerm: string = ''; // Pagination currentPage: number = 1; pageSize: number = 25; // Auto-refresh autoRefreshEnabled: boolean = true; private refreshSubscription?: Subscription; private readonly REFRESH_INTERVAL = 5000; // 5 seconds // Sorting sortColumn: string | null = null; sortDirection: 'asc' | 'desc' = 'asc'; loading: boolean = false; error: string | null = null; // Lucide icons readonly RefreshCw = RefreshCw; readonly Search = Search; readonly X = X; readonly Download = Download; readonly Trash2 = Trash2; readonly Sparkles = Sparkles; constructor(private artifactService: ArtifactService) {} ngOnInit() { this.loadArtifacts(); this.startAutoRefresh(); } ngOnDestroy() { this.stopAutoRefresh(); } loadArtifacts() { this.loading = true; this.error = null; const offset = (this.currentPage - 1) * this.pageSize; this.artifactService.listArtifacts(this.pageSize, offset).subscribe({ next: (artifacts) => { this.artifacts = artifacts; this.applyFilter(); this.loading = false; }, error: (err) => { this.error = 'Failed to load artifacts: ' + err.message; this.loading = false; } }); } applyFilter() { if (!this.searchTerm) { this.filteredArtifacts = [...this.artifacts]; } else { const term = this.searchTerm.toLowerCase(); this.filteredArtifacts = this.artifacts.filter(artifact => artifact.filename.toLowerCase().includes(term) || (artifact.test_name && artifact.test_name.toLowerCase().includes(term)) || (artifact.test_suite && artifact.test_suite.toLowerCase().includes(term)) || (artifact.sim_source_id && artifact.sim_source_id.toLowerCase().includes(term)) ); } if (this.sortColumn) { this.applySorting(); } } onSearch() { this.applyFilter(); } clearSearch() { this.searchTerm = ''; this.applyFilter(); } sortTable(column: string) { if (this.sortColumn === column) { this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; } else { this.sortColumn = column; this.sortDirection = 'asc'; } this.applySorting(); } applySorting() { if (!this.sortColumn) return; this.filteredArtifacts.sort((a, b) => { const aVal = (a as any)[this.sortColumn!] || ''; const bVal = (b as any)[this.sortColumn!] || ''; let comparison = 0; if (this.sortColumn === 'created_at') { comparison = new Date(aVal).getTime() - new Date(bVal).getTime(); } else { comparison = String(aVal).localeCompare(String(bVal)); } return this.sortDirection === 'asc' ? comparison : -comparison; }); } previousPage() { if (this.currentPage > 1) { this.currentPage--; this.loadArtifacts(); } } nextPage() { this.currentPage++; this.loadArtifacts(); } toggleAutoRefresh() { this.autoRefreshEnabled = !this.autoRefreshEnabled; if (this.autoRefreshEnabled) { this.startAutoRefresh(); } else { this.stopAutoRefresh(); } } private startAutoRefresh() { if (!this.autoRefreshEnabled) return; this.refreshSubscription = interval(this.REFRESH_INTERVAL) .pipe(switchMap(() => this.artifactService.listArtifacts(this.pageSize, (this.currentPage - 1) * this.pageSize))) .subscribe({ next: (artifacts) => { this.artifacts = artifacts; this.applyFilter(); }, error: (err) => console.error('Auto-refresh error:', err) }); } private stopAutoRefresh() { if (this.refreshSubscription) { this.refreshSubscription.unsubscribe(); } } showDetail(artifact: Artifact) { this.selectedArtifact = artifact; } closeDetail() { this.selectedArtifact = null; } downloadArtifact(artifact: Artifact, event: Event) { event.stopPropagation(); this.artifactService.downloadArtifact(artifact.id).subscribe({ next: (blob) => { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = artifact.filename; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); }, error: (err) => alert('Download failed: ' + err.message) }); } deleteArtifact(artifact: Artifact, event: Event) { event.stopPropagation(); if (!confirm(`Are you sure you want to delete ${artifact.filename}? This cannot be undone.`)) { return; } this.artifactService.deleteArtifact(artifact.id).subscribe({ next: () => { this.loadArtifacts(); if (this.selectedArtifact?.id === artifact.id) { this.closeDetail(); } }, error: (err) => alert('Delete failed: ' + err.message) }); } generateSeedData() { const count = prompt('How many artifacts to generate? (1-100)', '10'); if (!count) return; const num = parseInt(count); if (isNaN(num) || num < 1 || num > 100) { alert('Please enter a number between 1 and 100'); return; } this.artifactService.generateSeedData(num).subscribe({ next: (result) => { alert(result.message); this.loadArtifacts(); }, error: (err) => alert('Generation failed: ' + err.message) }); } formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; } formatDate(dateString: string): string { return new Date(dateString).toLocaleString(); } }