Add central Audit Trail viewer in Filament admin
Features: - AuditResource with table listing all audit logs - ViewAudit page showing detailed changes with diff table - Filters: date range, user, model type, event type - Search by user name - Auto-refresh every 60 seconds - Dark mode support for changes view - Fixed audits migration with proper schema - Added audit.view permission (admin only by default) Files: - src/app/Models/Audit.php - src/app/Filament/Resources/AuditResource.php - src/app/Filament/Resources/AuditResource/Pages/ - src/resources/views/filament/infolists/entries/audit-changes.blade.php - Updated audits migration with full schema
This commit is contained in:
231
src/app/Filament/Resources/AuditResource.php
Normal file
231
src/app/Filament/Resources/AuditResource.php
Normal file
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\AuditResource\Pages;
|
||||
use App\Models\Audit;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Infolists;
|
||||
use Filament\Infolists\Infolist;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class AuditResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Audit::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-clipboard-document-list';
|
||||
|
||||
protected static ?string $navigationGroup = 'System';
|
||||
|
||||
protected static ?string $navigationLabel = 'Audit Trail';
|
||||
|
||||
protected static ?int $navigationSort = 100;
|
||||
|
||||
protected static ?string $modelLabel = 'Audit Log';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'Audit Trail';
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()?->can('audit.view') ?? false;
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function canEdit($record): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function canDelete($record): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form->schema([]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->label('Date/Time')
|
||||
->dateTime('M j, Y H:i:s')
|
||||
->sortable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('user.name')
|
||||
->label('User')
|
||||
->default('System')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
|
||||
Tables\Columns\BadgeColumn::make('event')
|
||||
->label('Action')
|
||||
->colors([
|
||||
'success' => 'created',
|
||||
'warning' => 'updated',
|
||||
'danger' => 'deleted',
|
||||
'info' => 'restored',
|
||||
])
|
||||
->formatStateUsing(fn (string $state): string => ucfirst($state)),
|
||||
|
||||
Tables\Columns\TextColumn::make('auditable_type')
|
||||
->label('Model')
|
||||
->formatStateUsing(fn (string $state): string => class_basename($state))
|
||||
->sortable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('auditable_id')
|
||||
->label('Record ID')
|
||||
->sortable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('ip_address')
|
||||
->label('IP Address')
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
Tables\Columns\TextColumn::make('url')
|
||||
->label('URL')
|
||||
->limit(30)
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->defaultSort('created_at', 'desc')
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('event')
|
||||
->label('Action')
|
||||
->options([
|
||||
'created' => 'Created',
|
||||
'updated' => 'Updated',
|
||||
'deleted' => 'Deleted',
|
||||
'restored' => 'Restored',
|
||||
]),
|
||||
|
||||
Tables\Filters\SelectFilter::make('user_id')
|
||||
->label('User')
|
||||
->relationship('user', 'name')
|
||||
->searchable()
|
||||
->preload(),
|
||||
|
||||
Tables\Filters\SelectFilter::make('auditable_type')
|
||||
->label('Model')
|
||||
->options(fn () => Audit::query()
|
||||
->distinct()
|
||||
->pluck('auditable_type')
|
||||
->mapWithKeys(fn ($type) => [$type => class_basename($type)])
|
||||
->toArray()
|
||||
),
|
||||
|
||||
Tables\Filters\Filter::make('created_at')
|
||||
->form([
|
||||
Forms\Components\DatePicker::make('from')
|
||||
->label('From Date'),
|
||||
Forms\Components\DatePicker::make('until')
|
||||
->label('Until Date'),
|
||||
])
|
||||
->query(function (Builder $query, array $data): Builder {
|
||||
return $query
|
||||
->when(
|
||||
$data['from'],
|
||||
fn (Builder $query, $date): Builder => $query->whereDate('created_at', '>=', $date),
|
||||
)
|
||||
->when(
|
||||
$data['until'],
|
||||
fn (Builder $query, $date): Builder => $query->whereDate('created_at', '<=', $date),
|
||||
);
|
||||
})
|
||||
->indicateUsing(function (array $data): array {
|
||||
$indicators = [];
|
||||
if ($data['from'] ?? null) {
|
||||
$indicators['from'] = 'From: ' . $data['from'];
|
||||
}
|
||||
if ($data['until'] ?? null) {
|
||||
$indicators['until'] = 'Until: ' . $data['until'];
|
||||
}
|
||||
return $indicators;
|
||||
}),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\ViewAction::make(),
|
||||
])
|
||||
->bulkActions([])
|
||||
->poll('60s');
|
||||
}
|
||||
|
||||
public static function infolist(Infolist $infolist): Infolist
|
||||
{
|
||||
return $infolist
|
||||
->schema([
|
||||
Infolists\Components\Section::make('Audit Details')
|
||||
->schema([
|
||||
Infolists\Components\Grid::make(3)
|
||||
->schema([
|
||||
Infolists\Components\TextEntry::make('created_at')
|
||||
->label('Date/Time')
|
||||
->dateTime('F j, Y H:i:s'),
|
||||
|
||||
Infolists\Components\TextEntry::make('user.name')
|
||||
->label('User')
|
||||
->default('System'),
|
||||
|
||||
Infolists\Components\TextEntry::make('event')
|
||||
->label('Action')
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'created' => 'success',
|
||||
'updated' => 'warning',
|
||||
'deleted' => 'danger',
|
||||
'restored' => 'info',
|
||||
default => 'gray',
|
||||
})
|
||||
->formatStateUsing(fn (string $state): string => ucfirst($state)),
|
||||
]),
|
||||
|
||||
Infolists\Components\Grid::make(3)
|
||||
->schema([
|
||||
Infolists\Components\TextEntry::make('auditable_type')
|
||||
->label('Model')
|
||||
->formatStateUsing(fn (string $state): string => class_basename($state)),
|
||||
|
||||
Infolists\Components\TextEntry::make('auditable_id')
|
||||
->label('Record ID'),
|
||||
|
||||
Infolists\Components\TextEntry::make('ip_address')
|
||||
->label('IP Address')
|
||||
->default('N/A'),
|
||||
]),
|
||||
|
||||
Infolists\Components\TextEntry::make('url')
|
||||
->label('URL')
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
|
||||
Infolists\Components\Section::make('Changes')
|
||||
->schema([
|
||||
Infolists\Components\ViewEntry::make('changes')
|
||||
->view('filament.infolists.entries.audit-changes')
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListAudits::route('/'),
|
||||
'view' => Pages\ViewAudit::route('/{record}'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\AuditResource\Pages;
|
||||
|
||||
use App\Filament\Resources\AuditResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListAudits extends ListRecords
|
||||
{
|
||||
protected static string $resource = AuditResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
16
src/app/Filament/Resources/AuditResource/Pages/ViewAudit.php
Normal file
16
src/app/Filament/Resources/AuditResource/Pages/ViewAudit.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\AuditResource\Pages;
|
||||
|
||||
use App\Filament\Resources\AuditResource;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewAudit extends ViewRecord
|
||||
{
|
||||
protected static string $resource = AuditResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user