flare / flare-ui /src /app /components /activity-log /activity-log.component.ts
ciyidogan's picture
Upload 118 files
9f79da5 verified
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: `
<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 {
@Output() close = new EventEmitter<void>();
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.';
}
}