Spaces:
Running
Running
import { Component, EventEmitter, Output, inject, OnInit } 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'; | |
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 | |
], | |
template: ` | |
<div class="activity-log-dropdown" (click)="$event.stopPropagation()"> | |
<div class="activity-header"> | |
<h3>π Recent Activities</h3> | |
<button class="close-btn" (click)="close.emit()">Γ</button> | |
</div> | |
<div class="activity-content"> | |
@if (loading && activities.length === 0) { | |
<div class="loading"> | |
<mat-spinner diameter="30"></mat-spinner> | |
</div> | |
} @else if (activities.length === 0) { | |
<div class="empty">No activities found</div> | |
} @else { | |
<div class="activity-list"> | |
@for (activity of activities; track activity.id) { | |
<div class="activity-item"> | |
<div class="activity-time">{{ getRelativeTime(activity.timestamp) }}</div> | |
<div class="activity-content"> | |
<strong>{{ activity.user }}</strong> {{ getActionText(activity) }} | |
<em>{{ activity.entity_name }}</em> | |
@if (activity.details) { | |
<span class="details">β’ {{ activity.details }}</span> | |
} | |
</div> | |
</div> | |
} | |
</div> | |
} | |
</div> | |
<div class="activity-footer"> | |
@if (totalItems > pageSize) { | |
<mat-paginator | |
[length]="totalItems" | |
[pageSize]="pageSize" | |
[pageIndex]="currentPage - 1" | |
[pageSizeOptions]="[10, 25, 50]" | |
(page)="onPageChange($event)" | |
showFirstLastButtons> | |
</mat-paginator> | |
} @else { | |
<button mat-button (click)="openFullView()"> | |
<mat-icon>open_in_new</mat-icon> | |
View All Activities | |
</button> | |
} | |
</div> | |
</div> | |
`, | |
styles: [` | |
.activity-log-dropdown { | |
position: absolute; | |
top: 100%; | |
right: 0; | |
width: 400px; | |
background: white; | |
border: 1px solid #dee2e6; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); | |
z-index: 1000; | |
margin-top: 0.5rem; | |
display: flex; | |
flex-direction: column; | |
max-height: 600px; | |
} | |
.activity-header { | |
padding: 1rem; | |
border-bottom: 1px solid #dee2e6; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
h3 { | |
margin: 0; | |
font-size: 1.1rem; | |
} | |
.close-btn { | |
background: none; | |
border: none; | |
font-size: 1.5rem; | |
cursor: pointer; | |
color: #6c757d; | |
line-height: 1; | |
padding: 0; | |
width: 24px; | |
height: 24px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
&:hover { | |
color: #333; | |
} | |
} | |
} | |
.activity-content { | |
flex: 1; | |
overflow-y: auto; | |
min-height: 200px; | |
max-height: 400px; | |
} | |
.activity-list { | |
.activity-item { | |
padding: 0.75rem 1rem; | |
border-bottom: 1px solid #f0f0f0; | |
transition: background-color 0.2s; | |
&:hover { | |
background-color: #f8f9fa; | |
} | |
&:last-child { | |
border-bottom: none; | |
} | |
.activity-time { | |
font-size: 0.85rem; | |
color: #6c757d; | |
margin-bottom: 0.25rem; | |
} | |
.activity-content { | |
font-size: 0.9rem; | |
em { | |
color: #007bff; | |
font-style: normal; | |
font-weight: 500; | |
} | |
.details { | |
color: #6c757d; | |
font-size: 0.85rem; | |
} | |
} | |
} | |
} | |
.activity-footer { | |
padding: 0.5rem; | |
border-top: 1px solid #dee2e6; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
button { | |
width: 100%; | |
} | |
::ng-deep .mat-paginator { | |
background: transparent; | |
width: 100%; | |
.mat-paginator-container { | |
padding: 0; | |
justify-content: center; | |
} | |
.mat-paginator-range-label { | |
margin: 0 8px; | |
} | |
} | |
} | |
.loading { | |
padding: 3rem; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
} | |
.empty { | |
padding: 3rem; | |
text-align: center; | |
color: #6c757d; | |
} | |
`] | |
}) | |
export class ActivityLogComponent implements OnInit { | |
new EventEmitter<void>(); | () close =|
private http = inject(HttpClient); | |
activities: ActivityLog[] = []; | |
loading = false; | |
currentPage = 1; | |
pageSize = 10; | |
totalItems = 0; | |
totalPages = 0; | |
ngOnInit() { | |
this.loadActivities(); | |
} | |
loadActivities(page: number = 1) { | |
this.loading = true; | |
this.currentPage = page; | |
this.http.get<ActivityLogResponse>( | |
`/api/activity-log?page=${page}&limit=${this.pageSize}` | |
).subscribe({ | |
next: (response) => { | |
this.activities = response.items; | |
this.totalItems = response.total; | |
this.totalPages = response.pages; | |
this.loading = false; | |
}, | |
error: (error) => { | |
console.error('Failed to load activities:', error); | |
this.loading = false; | |
} | |
}); | |
} | |
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(); | |
} | |
getRelativeTime(timestamp: string): string { | |
const date = new Date(timestamp); | |
const now = new Date(); | |
const diff = now.getTime() - date.getTime(); | |
const minutes = Math.floor(diff / 60000); | |
const hours = Math.floor(diff / 3600000); | |
const days = Math.floor(diff / 86400000); | |
if (minutes < 1) return 'just now'; | |
if (minutes < 60) return `${minutes} min ago`; | |
if (hours < 24) return `${hours} hour${hours > 1 ? 's' : ''} ago`; | |
if (days < 7) return `${days} day${days > 1 ? 's' : ''} ago`; | |
return date.toLocaleDateString(); | |
} | |
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' | |
}; | |
return actions[activity.action] || activity.action.toLowerCase().replace(/_/g, ' '); | |
} | |
} |