Spaces:
Building
Building
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; | |
} | |
({ | |
selector: 'app-activity-log', | |
standalone: true, | |
imports: [ | |
CommonModule, | |
MatProgressSpinnerModule, | |
MatButtonModule, | |
MatIconModule, | |
MatPaginatorModule, | |
MatListModule, | |
MatCardModule, | |
MatDividerModule, | |
MatSnackBarModule | |
], | |
template: ` | |
<mat-card class="activity-log-dropdown" (click)="$event.stopPropagation()"> | |
<mat-card-header> | |
<mat-card-title> | |
<mat-icon>notifications</mat-icon> | |
Recent Activities | |
</mat-card-title> | |
<button mat-icon-button (click)="close.emit(); $event.stopPropagation()"> | |
<mat-icon>close</mat-icon> | |
</button> | |
</mat-card-header> | |
<mat-card-content> | |
@if (loading && activities.length === 0) { | |
<div class="loading"> | |
<mat-spinner diameter="30"></mat-spinner> | |
</div> | |
} @else if (error && activities.length === 0) { | |
<div class="error-state"> | |
<mat-icon>error_outline</mat-icon> | |
<p>{{ error }}</p> | |
<button mat-button (click)="retry()"> | |
<mat-icon>refresh</mat-icon> | |
Retry | |
</button> | |
</div> | |
} @else if (activities.length === 0) { | |
<div class="empty"> | |
<mat-icon>inbox</mat-icon> | |
<p>No activities found</p> | |
</div> | |
} @else { | |
<mat-list class="activity-list"> | |
@for (activity of activities; track activity.id) { | |
<mat-list-item> | |
<mat-icon matListItemIcon>{{ getActivityIcon(activity.action) }}</mat-icon> | |
<div matListItemTitle> | |
<span class="activity-time">{{ getRelativeTime(activity.timestamp) }}</span> | |
</div> | |
<div matListItemLine> | |
<strong>{{ activity.user }}</strong> {{ getActionText(activity) }} | |
<em>{{ activity.entity_name }}</em> | |
@if (activity.details) { | |
<span class="details">• {{ activity.details }}</span> | |
} | |
</div> | |
</mat-list-item> | |
@if (!$last) { | |
<mat-divider></mat-divider> | |
} | |
} | |
</mat-list> | |
} | |
</mat-card-content> | |
<mat-card-actions *ngIf="totalItems > pageSize"> | |
<mat-paginator | |
[length]="totalItems" | |
[pageSize]="pageSize" | |
[pageIndex]="currentPage - 1" | |
[pageSizeOptions]="[10, 25, 50]" | |
(page)="onPageChange($event)" | |
showFirstLastButtons> | |
</mat-paginator> | |
</mat-card-actions> | |
<mat-card-actions *ngIf="totalItems <= pageSize && activities.length > 0"> | |
<button mat-button (click)="openFullView()" class="full-width"> | |
<mat-icon>open_in_new</mat-icon> | |
View All Activities | |
</button> | |
</mat-card-actions> | |
</mat-card> | |
`, | |
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 { | |
new EventEmitter<void>(); | () close =|
private http = inject(HttpClient); | |
private snackBar = inject(MatSnackBar); | |
private destroyed$ = new Subject<void>(); | |
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<ActivityLog[]>( | |
`/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<string, string> = { | |
'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.'; | |
} | |
} |