import { Component, EventEmitter, Output, inject, OnInit, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { HttpClient } from '@angular/common/http'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatPaginatorModule, PageEvent } from '@angular/material/paginator'; import { MatListModule } from '@angular/material/list'; import { MatCardModule } from '@angular/material/card'; import { MatDividerModule } from '@angular/material/divider'; import { Subject, takeUntil } from 'rxjs'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; interface ActivityLog { id: number; timestamp: string; user: string; action: string; entity_type: string; entity_id: any; entity_name: string; details?: string; } interface ActivityLogResponse { items: ActivityLog[]; total: number; page: number; limit: number; pages: number; } @Component({ selector: 'app-activity-log', standalone: true, imports: [ CommonModule, MatProgressSpinnerModule, MatButtonModule, MatIconModule, MatPaginatorModule, MatListModule, MatCardModule, MatDividerModule, MatSnackBarModule ], template: ` notifications Recent Activities @if (loading && activities.length === 0) {
} @else if (error && activities.length === 0) {
error_outline

{{ error }}

} @else if (activities.length === 0) {
inbox

No activities found

} @else { @for (activity of activities; track activity.id) { {{ getActivityIcon(activity.action) }}
{{ getRelativeTime(activity.timestamp) }}
{{ activity.user }} {{ getActionText(activity) }} {{ activity.entity_name }} @if (activity.details) { • {{ activity.details }} }
@if (!$last) { } }
}
`, styles: [` .activity-log-dropdown { width: 450px; max-height: 600px; display: flex; flex-direction: column; overflow: hidden; } mat-card-header { display: flex; justify-content: space-between; align-items: center; padding: 16px; background-color: #424242; color: white; mat-card-title { margin: 0; display: flex; align-items: center; gap: 8px; font-size: 18px; color: white; mat-icon { font-size: 24px; width: 24px; height: 24px; color: white; } } button { color: white; } } mat-card-content { flex: 1; overflow-y: auto; padding: 0; min-height: 200px; max-height: 400px; } .activity-list { padding: 0; mat-list-item { height: auto; min-height: 72px; padding: 12px 16px; &:hover { background-color: #f5f5f5; } .activity-time { font-size: 12px; color: #666; } strong { color: #1976d2; margin-right: 4px; } em { color: #673ab7; font-style: normal; font-weight: 500; margin: 0 4px; } .details { color: #666; font-size: 12px; margin-left: 4px; } } } mat-card-actions { padding: 0; margin: 0; mat-paginator { background: transparent; } .full-width { width: 100%; margin: 0; } } .loading, .empty, .error-state { padding: 60px 20px; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #666; mat-icon { font-size: 48px; width: 48px; height: 48px; color: #e0e0e0; margin-bottom: 16px; } p { margin: 0 0 16px; font-size: 14px; } } .error-state { mat-icon { color: #f44336; } } ::ng-deep { .mat-mdc-list-item-unscoped-content { display: block; } .mat-mdc-paginator { .mat-mdc-paginator-container { padding: 8px; justify-content: center; } } } `] }) export class ActivityLogComponent implements OnInit, OnDestroy { @Output() close = new EventEmitter(); private http = inject(HttpClient); private snackBar = inject(MatSnackBar); private destroyed$ = new Subject(); activities: ActivityLog[] = []; loading = false; error = ''; currentPage = 1; pageSize = 10; totalItems = 0; totalPages = 0; ngOnInit() { this.loadActivities(); } ngOnDestroy() { this.destroyed$.next(); this.destroyed$.complete(); } loadActivities(page: number = 1) { this.loading = true; this.error = ''; this.currentPage = page; // Backend sadece limit parametresi alıyor, page almıyor const limit = this.pageSize * page; // Toplam kaç kayıt istediğimizi hesapla this.http.get( `/api/activity-log?limit=${limit}` ).pipe( takeUntil(this.destroyed$) ).subscribe({ next: (response) => { try { // Response direkt array olarak geliyor const allActivities = response || []; // Manual pagination yap const startIndex = (page - 1) * this.pageSize; const endIndex = startIndex + this.pageSize; this.activities = allActivities.slice(startIndex, endIndex); this.totalItems = allActivities.length; this.totalPages = Math.ceil(allActivities.length / this.pageSize); this.loading = false; } catch (err) { console.error('Failed to process activities:', err); this.error = 'Failed to process activity data'; this.activities = []; this.loading = false; } }, error: (error) => { console.error('Failed to load activities:', error); this.error = this.getErrorMessage(error); this.activities = []; this.loading = false; // Show error in snackbar this.snackBar.open(this.error, 'Close', { duration: 5000, panelClass: 'error-snackbar' }); } }); } onPageChange(event: PageEvent) { this.pageSize = event.pageSize; this.loadActivities(event.pageIndex + 1); } openFullView() { // TODO: Implement full activity log view console.log('Open full activity log view'); this.close.emit(); } retry() { this.loadActivities(this.currentPage); } getRelativeTime(timestamp: string): string { try { const date = new Date(timestamp); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMs < 0) return 'just now'; // Future dates if (diffMins < 1) return 'just now'; if (diffMins < 60) return `${diffMins} min ago`; if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`; if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`; return date.toLocaleDateString(); } catch (err) { console.error('Invalid timestamp:', timestamp, err); return 'Unknown'; } } getActionText(activity: ActivityLog): string { const actions: Record = { 'CREATE_PROJECT': 'created project', 'UPDATE_PROJECT': 'updated project', 'DELETE_PROJECT': 'deleted project', 'ENABLE_PROJECT': 'enabled project', 'DISABLE_PROJECT': 'disabled project', 'PUBLISH_VERSION': 'published version of', 'CREATE_VERSION': 'created version for', 'UPDATE_VERSION': 'updated version of', 'DELETE_VERSION': 'deleted version from', 'CREATE_API': 'created API', 'UPDATE_API': 'updated API', 'DELETE_API': 'deleted API', 'UPDATE_ENVIRONMENT': 'updated environment', 'IMPORT_PROJECT': 'imported project', 'CHANGE_PASSWORD': 'changed password', 'LOGIN': 'logged in', 'LOGOUT': 'logged out', 'FAILED_LOGIN': 'failed login attempt' }; return actions[activity.action] || activity.action.toLowerCase().replace(/_/g, ' '); } getActivityIcon(action: string): string { if (action.includes('CREATE')) return 'add_circle'; if (action.includes('UPDATE')) return 'edit'; if (action.includes('DELETE')) return 'delete'; if (action.includes('ENABLE')) return 'check_circle'; if (action.includes('DISABLE')) return 'cancel'; if (action.includes('PUBLISH')) return 'publish'; if (action.includes('IMPORT')) return 'cloud_upload'; if (action.includes('PASSWORD')) return 'lock'; if (action.includes('LOGIN')) return 'login'; if (action.includes('LOGOUT')) return 'logout'; return 'info'; } trackByActivityId(index: number, activity: ActivityLog): number { return activity.id; } isLast(activity: ActivityLog): boolean { return this.activities.indexOf(activity) === this.activities.length - 1; } private getErrorMessage(error: any): string { if (error.status === 0) { return 'Unable to connect to server. Please check your connection.'; } else if (error.status === 401) { return 'Session expired. Please login again.'; } else if (error.status === 403) { return 'You do not have permission to view activity logs.'; } else if (error.error?.detail) { return error.error.detail; } else if (error.message) { return error.message; } return 'Failed to load activities. Please try again.'; } }