ole(['admin', 'support', 'data-analyst']);
}
/**
* Determine if the user can view a specific import record.
* We restrict access to completed imports that actually have failures.
*/
public function view(User $user, Import $import): bool
{
if (is_null($import->completed_at)) {
return false;
}
if ($import->getFailedRowsCount() === 0) {
return false;
}
return $user->can('view_import_errors');
}
/**
* Determine if the user can delete import records.
*/
public function delete(User $user, Import $import): bool
{
return $user->hasRole('admin');
}
}
**Rationale:**
* **Safety Gate:** The `view` method checks `completed_at` and `getFailedRowsCount()`. This prevents users from downloading empty files or attempting to access files while the import job is still running.
* **Granular Permissions:** Separating `viewAny` and `view` allows you to grant list access to analysts while restricting error downloads to support staff.
#### 2. Extending the Import Model for Secure Downloads
Filament provides an internal signed route for downloading failed rows: `filament.imports.failed-rows.download`. To keep the resource clean and encapsulate URL generation, we extend the `Import` model with a helper method.
```php
namespace App\Models;
use Filament\Actions\Imports\Models\Import as FilamentImport;
use Illuminate\Support\Facades\URL;
class Import extends FilamentImport
{
/**
* Generate a signed URL for downloading the failed rows CSV.
* This leverages Filament's internal controller while keeping
* the logic encapsulated within the model.
*/
public function getSignedErrorUrl(): string
{
return URL::signedRoute(
'filament.imports.failed-rows.download',
['import' => $this->id]
);
}
}
Rationale:
- Encapsulation: The resource table definition remains clean. The URL generation logic is tied to the model, making it reusable if you need to email error links or expose them via API later.
- Security: Using
URL::signedRoute ensures the link is tamper-proof and can be configured with expiration times if needed.
3. Building the Audit Resource
Create a Filament resource to list imports. Since we only need to view and download errors, we skip the create/edit forms.
namespace App\Filament\Resources;
use App\Filament\Resources\ImportResource\Pages;
use App\Models\Import;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Columns\BadgeColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\Filter;
use Filament\Tables\Table;
class ImportResource extends Resource
{
protected static ?string $model = Import::class;
protected static ?string $navigationIcon = 'heroicon-o-arrow-down-tray';
protected static ?string $navigationLabel = 'Import History';
public static function form(Form $form): Form
{
return $form->schema([]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('file_name')
->searchable()
->limit(30),
TextColumn::make('user.name')
->label('Initiated By')
->searchable(),
BadgeColumn::make('status')
->label('Status')
->colors([
'success' => 'completed',
'warning' => 'processing',
'danger' => 'failed',
])
->formatStateUsing(fn (Import $record) => match (true) {
is_null($record->completed_at) => 'Processing',
$record->getFailedRowsCount() > 0 => 'Completed with Errors',
default => 'Completed',
}),
TextColumn::make('successful_rows')
->label('Success')
->numeric()
->sortable(),
TextColumn::make('getFailedRowsCount')
->label('Failures')
->numeric()
->sortable()
->color(fn (Import $record) => $record->getFailedRowsCount() > 0 ? 'danger' : null),
TextColumn::make('completed_at')
->label('Finished')
->dateTime()
->sortable(),
])
->filters([
Filter::make('has_errors')
->query(fn ($query) => $query->whereNotNull('completed_at')
->whereRaw('total_rows - successful_rows > 0'))
->label('Only Imports with Errors'),
])
->actions([
Action::make('downloadErrors')
->label('Download Errors')
->icon('heroicon-m-arrow-down-tray')
->color('warning')
->url(fn (Import $record) => $record->getSignedErrorUrl())
->openUrlInNewTab()
->authorize('view')
->visible(fn (Import $record) => $record->getFailedRowsCount() > 0),
DeleteAction::make()
->authorize('delete'),
])
->defaultSort('created_at', 'desc');
}
public static function getPages(): array
{
return [
'index' => Pages\ListImports::route('/'),
];
}
}
Rationale:
- Status Badge: A computed
BadgeColumn provides immediate visual feedback on import health without requiring users to inspect row counts.
- Smart Visibility: The download action uses
visible() to hide the button when there are no errors, reducing UI clutter.
- Authorization Sync: The action uses
->authorize('view'), which delegates to the policy's view method. This ensures the safety checks defined in the policy are enforced at the UI level.
- Filters: The
has_errors filter allows support teams to quickly isolate problematic imports.
Pitfall Guide
1. Policy Registration Omission
2. Downloading Incomplete Imports
- Explanation: Users may click the download action while the import job is still processing. This can result in partial files or errors.
- Fix: The policy
view method must check is_null($import->completed_at). The resource action should also use visible() to hide the button based on status.
3. Signed URL Expiration
- Explanation: Signed URLs generated by
URL::signedRoute have a default expiration. If users bookmark links or if links are cached, they may expire before use.
- Fix: Generate URLs on-demand via the action closure rather than storing them. If long-lived links are required, pass an expiration parameter to
signedRoute, though this is rarely recommended for security-sensitive error files.
4. Performance Degradation on Large Tables
- Explanation: Import records accumulate over time. Loading thousands of records without pagination or indexing can slow down the resource.
- Fix: Ensure
created_at and completed_at are indexed in the database. Use Filament's default pagination. Consider implementing soft deletes or a cleanup job to archive old imports.
5. Authorization Mismatch
- Explanation: The table action uses
->authorize('view'), but the policy defines canDownload or a differently named method.
- Fix: Ensure the string passed to
authorize() matches the method name in the policy. Filament maps authorize('view') to view($user, $model).
6. Missing Model Namespace in Resource Generation
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Small Team / Low Volume | Default Notification | Simpler; no extra code required. | Zero |
| Support Team Needs Access | Centralized Audit Resource | Enables support to resolve issues without user involvement. | Low (Dev time) |
| Compliance / Audit Requirements | Centralized Audit Resource | Provides persistent history and access logs. | Low (Dev time) |
| Automated Import Pipelines | Centralized Audit Resource + Webhooks | Allows monitoring of automated jobs and alerting on failures. | Medium (Integration) |
| High Volume / Performance Critical | Audit Resource + Soft Deletes | Prevents table bloat; maintains performance over time. | Low (Storage/Job) |
Configuration Template
Policy Registration (AuthServiceProvider.php):
use Illuminate\Support\Facades\Gate;
use Filament\Actions\Imports\Models\Import;
use App\Policies\ImportAuditPolicy;
public function boot(): void
{
$this->registerPolicies();
Gate::policy(Import::class, ImportAuditPolicy::class);
}
Resource Table Action Snippet:
Action::make('downloadErrors')
->label('Download Errors')
->icon('heroicon-m-arrow-down-tray')
->color('warning')
->url(fn (Import $record) => $record->getSignedErrorUrl())
->openUrlInNewTab()
->authorize('view')
->visible(fn (Import $record) => $record->getFailedRowsCount() > 0),
Quick Start Guide
- Generate Policy:
php artisan make:policy ImportAuditPolicy --model="Filament\Actions\Imports\Models\Import"
- Register Policy: Add
Gate::policy(Import::class, ImportAuditPolicy::class); to AuthServiceProvider.
- Generate Resource:
php artisan make:filament-resource Import --model-namespace="Filament\Actions\Imports\Models"
- Configure Resource: Update
ImportResource with the table definition and download action from the Core Solution.
- Verify: Navigate to the new "Import History" resource in Filament. Confirm that authorized users can see imports and download error CSVs, while unauthorized users receive 403 errors.