diff --git a/frontend/src/app/components/audit/audit-data-source.ts b/frontend/src/app/components/audit/audit-data-source.ts index 0ef494a76..63a5cd5b9 100644 --- a/frontend/src/app/components/audit/audit-data-source.ts +++ b/frontend/src/app/components/audit/audit-data-source.ts @@ -1,11 +1,12 @@ import { BehaviorSubject, Observable, of } from 'rxjs'; import { catchError, finalize } from 'rxjs/operators'; +import { format, isToday } from 'date-fns'; import { CollectionViewer } from '@angular/cdk/collections'; import { ConnectionsService } from 'src/app/services/connections.service'; import { DataSource } from '@angular/cdk/table'; import { MatPaginator } from '@angular/material/paginator'; -import { format } from 'date-fns' +import { UserService } from 'src/app/services/user.service'; interface Column { title: string, @@ -28,7 +29,7 @@ export class AuditDataSource implements DataSource { public loading$ = this.loadingSubject.asObservable(); public paginator: MatPaginator; - constructor(private _connections: ConnectionsService) {} + constructor(private _connections: ConnectionsService, private _userService: UserService) {} connect(collectionViewer: CollectionViewer): Observable { return this.rowsSubject.asObservable(); @@ -39,12 +40,40 @@ export class AuditDataSource implements DataSource { this.loadingSubject.complete(); } - fetchLogs({ + async fetchLogs({ connectionID, tableName, - userEmail - }: RowsParams) { + userEmail, + usersList + }: RowsParams & { usersList?: any[] }) { this.loadingSubject.next(true); + let effectiveUsersList = usersList; + if (!effectiveUsersList || !Array.isArray(effectiveUsersList) || effectiveUsersList.length === 0) { + try { + const stored = localStorage.getItem('usersList'); + if (stored) { + effectiveUsersList = JSON.parse(stored); + } + } catch (e) {} + if ((!effectiveUsersList || !Array.isArray(effectiveUsersList) || effectiveUsersList.length === 0) && (window as any).usersList) { + effectiveUsersList = (window as any).usersList; + } + } + // Always refresh user info before using + await this._userService.fetchUser().toPromise(); + let currentUserName = ''; + let currentUserEmail = ''; + const userValue = this._userService['user']?.getValue?.(); + console.log('DEBUG: current user from UserService', userValue); + if (userValue) { + currentUserName = userValue.name || ''; + currentUserEmail = userValue.email || ''; + } + // DEBUG LOGGING + console.log('DEBUG: effectiveUsersList', effectiveUsersList); + if (effectiveUsersList && Array.isArray(effectiveUsersList)) { + effectiveUsersList.forEach(u => console.log('DEBUG: user in usersList', u)); + } const fetchedLogs = this._connections.fetchAuditLog({ connectionID, tableName, @@ -60,24 +89,57 @@ export class AuditDataSource implements DataSource { finalize(() => this.loadingSubject.next(false)) ) .subscribe((res: any) => { - const actions = { addRow: 'added row', deleteRow: 'deleted row', updateRow: 'edit row', rowReceived: 'received row', - rowsReceived: 'received rows' + rowsReceived: 'received rows', + ruleAction: (actionName: string) => actionName } const formattedLogs = res.logs.map(log => { + if (log.operationType === 'actionActivated') { + console.log('DEBUG: actionActivated log', log); + } const date = new Date(log.createdAt); - const formattedDate = format(date, "P p") + const formattedDate = format(date, "d MMM yyyy, p"); + let name = ''; + let email = log.email || ''; + // Use current user name if email matches + if (currentUserEmail && email === currentUserEmail) { + name = currentUserName; + } else if (effectiveUsersList && Array.isArray(effectiveUsersList)) { + const userProfile = effectiveUsersList.find(u => u.email === email); + if (userProfile && userProfile.name) { + name = userProfile.name; + } + } + if (!name && log.name) name = log.name; + + // Handle action name + let actionName = log.operationType; + let actionIcon = null; + + if (log.operationType === 'actionActivated' && log.actionName) { + actionName = log.actionName; + actionIcon = log.icon; + } else if (log.operationType === 'ruleAction' && log.actionName) { + actionName = actions.ruleAction(log.actionName); + actionIcon = log.icon || 'rule'; + } else { + actionName = actions[log.operationType] || log.operationType; + } + return { ['Table']: log.table_name, - ['User']: log.email, - ['Action']: actions[log.operationType], + ['User']: name || email, + name: name, + email: email, + ['Action']: actionName, ['Date']: formattedDate, ['Status']: log.operationStatusResult, operationType: log.operationType, + actionIcon: actionIcon, createdAt: log.createdAt, prevValue: log.old_data, currentValue: log.received_data, diff --git a/frontend/src/app/components/audit/audit.component.css b/frontend/src/app/components/audit/audit.component.css index 4e5bf39de..5955f83f6 100644 --- a/frontend/src/app/components/audit/audit.component.css +++ b/frontend/src/app/components/audit/audit.component.css @@ -60,11 +60,32 @@ header { th.mat-header-cell, td.mat-cell { padding-right: 20px; + height: 44px; } -.table-cell-content_break-words { - padding: 16px 0; - word-break: break-all; +.table-cell-content { + padding: 4px 0; +} + +.date-cell { + color: rgba(0, 0, 0, 0.6); +} + +.status-badge { + display: inline-block; + padding: 4px 8px; + border-radius: 4px; + font-size: 14px; +} + +.status-badge.success { + background: #e8f5e9; + color: #1B5E20; +} + +.status-badge.error { + background: #ffebee; + color: #B71C1C; } .table-wrapper { @@ -110,4 +131,70 @@ th.mat-header-cell, td.mat-cell { .hidden { display: none; +} + +.action-cell { + display: flex; + align-items: center; + gap: 8px; +} + +.action-icon { + font-size: 18px; + width: 18px; + height: 18px; + color: #888; +} + +tr.mat-mdc-row, tr.mat-mdc-header-row { + height: 44px !important; + max-height: 44px !important; +} + +td.mat-mdc-cell, th.mat-mdc-header-cell { + height: 44px !important; + max-height: 44px !important; + padding-top: 0 !important; + padding-bottom: 0 !important; + vertical-align: middle !important; + line-height: normal !important; +} + +tr.mat-mdc-row:hover { + background: #f5f5f5 !important; + cursor: default !important; +} + +th.mat-header-cell { + background: #fff; +} + +@media (prefers-color-scheme: dark) { + th.mat-header-cell { + background: #202020 !important; + box-shadow: none !important; + border-bottom: none !important; + filter: none !important; + } + .mat-table, .table-wrapper, tr.mat-mdc-row, td.mat-mdc-cell { + background: #202020 !important; + } + tr.mat-mdc-row:hover { + background: var(--color-primaryPalette-800) !important; + cursor: default !important; + } + td.mat-mdc-cell.date-cell, .date-cell { + color: #888 !important; + } + .action-icon { + color: #888 !important; + } + .status-badge.success { + background: rgba(76, 175, 80, 0.15); + color: #4CAF50; + } + .status-badge.error { + background: rgba(229, 57, 53, 0.15); + color: #E53935; + } } \ No newline at end of file diff --git a/frontend/src/app/components/audit/audit.component.html b/frontend/src/app/components/audit/audit.component.html index 37b4601e2..b21d16303 100644 --- a/frontend/src/app/components/audit/audit.component.html +++ b/frontend/src/app/components/audit/audit.component.html @@ -72,20 +72,53 @@

Rocketadmin can not find any tables

{{ column }} -
- {{element[column] || '—'}} +
+ + + {{element[column] || '—'}} + + + + +
+ + {{getActionIcon(element.operationType, element.actionIcon)}} + + {{element[column] || '—'}} +
+
+
+ + + + {{element.name}} + {{element.email || element[column] || '—'}} + + + + + + {{(element[column] || '').split(',')[0]}} at {{(element[column] || '').split(',')[1]?.trim()}} + + + + {{element[column] || '—'}} +
- Details + Changes diff --git a/frontend/src/app/components/audit/audit.component.ts b/frontend/src/app/components/audit/audit.component.ts index efff0fc26..983c140f0 100644 --- a/frontend/src/app/components/audit/audit.component.ts +++ b/frontend/src/app/components/audit/audit.component.ts @@ -18,6 +18,7 @@ import { MatPaginator } from '@angular/material/paginator'; import { MatPaginatorModule } from '@angular/material/paginator'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; +import { MatIconModule } from '@angular/material/icon'; import { PlaceholderTableDataComponent } from '../skeletons/placeholder-table-data/placeholder-table-data.component'; import { RouterModule } from '@angular/router'; import { ServerError } from 'src/app/models/alert'; @@ -28,6 +29,7 @@ import { User } from '@sentry/angular-ivy'; import { UsersService } from 'src/app/services/users.service'; import { environment } from 'src/environments/environment'; import { normalizeTableName } from 'src/app/lib/normalize'; +import { UserService } from 'src/app/services/user.service'; @Component({ selector: 'app-audit', @@ -42,6 +44,7 @@ import { normalizeTableName } from 'src/app/lib/normalize'; MatButtonModule, MatTableModule, MatPaginatorModule, + MatIconModule, FormsModule, RouterModule, Angulartics2OnModule, @@ -76,7 +79,8 @@ export class AuditComponent implements OnInit { private _users: UsersService, private _companyService: CompanyService, public dialog: MatDialog, - private title: Title + private title: Title, + private _userService: UserService ) { } ngAfterViewInit() { @@ -99,7 +103,7 @@ export class AuditComponent implements OnInit { this.accesLevel = this._connections.currentConnectionAccessLevel; this.columns = ['Table', 'User', 'Action', 'Date', 'Status', 'Details']; this.dataColumns = ['Table', 'User', 'Action', 'Date', 'Status']; - this.dataSource = new AuditDataSource(this._connections); + this.dataSource = new AuditDataSource(this._connections, this._userService); this.loadLogsPage(); this._tables.fetchTables(this.connectionID) @@ -128,7 +132,8 @@ export class AuditComponent implements OnInit { this.dataSource.fetchLogs({ connectionID: this.connectionID, tableName: this.tableName, - userEmail: this.userEmail + userEmail: this.userEmail, + usersList: this.usersList }); } @@ -143,4 +148,29 @@ export class AuditComponent implements OnInit { // @ts-ignore Intercom('show'); } + + getActionIcon(operationType: string, actionIcon?: string): string { + if ((operationType === 'ruleAction' || operationType === 'actionActivated') && actionIcon) { + return actionIcon; + } + + switch (operationType) { + case 'addRow': + return 'add'; + case 'deleteRow': + return 'delete'; + case 'updateRow': + return 'edit'; + case 'rowReceived': + return 'visibility_outline'; + case 'rowsReceived': + return 'visibility_outline'; + case 'ruleAction': + return 'rule'; + case 'actionActivated': + return 'rule'; + default: + return 'info'; + } + } }