TenantAtlas/app/Filament/Resources/BackupScheduleResource/RelationManagers/BackupScheduleOperationRunsRelationManager.php
ahmido a4f5c4f122 Spec 125: standardize Filament table UX (#152)
## Summary
- standardize Filament table defaults across resources, relation managers, widgets, custom pages, and picker tables
- add shared pagination profiles, calm default column visibility, explicit empty states, and session persistence on designated critical resource lists
- complete Spec 125 artifacts, regression tests, and dashboard widget follow-up for lazy loading, sortable columns, and toggleable detail columns

## Verification
- `docker exec tenantatlas-laravel.test-1 php artisan test --compact --filter=BaselineCompareNow`
- `docker exec tenantatlas-laravel.test-1 php artisan test --compact --filter=TableStandardsBaseline`
- `docker exec tenantatlas-laravel.test-1 php artisan test --compact --filter=TableDetailVisibility`
- `docker exec tenantatlas-laravel.test-1 php artisan test --compact --filter=FilamentTableRiskExceptions`
- full suite run completed: `2017 passed, 10 failed, 8 skipped`
- manual browser QA completed on the tenant dashboard for lazy loading, sortable widget columns, toggleable hidden status columns, badges, and pagination

## Known Failures
The full suite still has 10 pre-existing failures unrelated to this branch:
- `Tests\\Unit\\OpsUx\\SummaryCountsNormalizerTest`
- `Tests\\Feature\\BackupWithAssignmentsConsistencyTest` (2 tests)
- `Tests\\Feature\\BaselineDriftEngine\\CaptureBaselineContentTest`
- `Tests\\Feature\\BaselineDriftEngine\\CompareContentEvidenceTest`
- `Tests\\Feature\\BaselineDriftEngine\\ResolverTest`
- `Tests\\Feature\\Filament\\TenantDashboardDbOnlyTest`
- `Tests\\Feature\\Operations\\ReconcileAdapterRunsJobTrackingTest`
- `Tests\\Feature\\ReviewPack\\ReviewPackRbacTest`
- `Tests\\Feature\\Verification\\VerificationReportRedactionTest`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #152
2026-03-08 22:54:56 +00:00

101 lines
4.6 KiB
PHP

<?php
namespace App\Filament\Resources\BackupScheduleResource\RelationManagers;
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Support\Badges\BadgeDomain;
use App\Support\Badges\BadgeRenderer;
use App\Support\OperationCatalog;
use App\Support\OperationRunLinks;
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
use Filament\Actions;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
class BackupScheduleOperationRunsRelationManager extends RelationManager
{
protected static string $relationship = 'operationRuns';
protected static ?string $title = 'Executions';
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
{
return ActionSurfaceDeclaration::forRelationManager(ActionSurfaceProfile::RelationManager)
->exempt(ActionSurfaceSlot::ListHeader, 'Executions relation list has no header actions.')
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ViewAction->value)
->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'Single primary row action is exposed, so no row menu is needed.')
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'Bulk actions are intentionally omitted for operation run safety.')
->exempt(ActionSurfaceSlot::ListEmptyState, 'No empty-state actions are exposed in this embedded relation manager.');
}
public function table(Table $table): Table
{
return $table
->modifyQueryUsing(fn (Builder $query) => $query->where('tenant_id', Tenant::currentOrFail()->getKey()))
->defaultSort('created_at', 'desc')
->paginated(\App\Support\Filament\TablePaginationProfiles::relationManager())
->columns([
Tables\Columns\TextColumn::make('created_at')
->label('Enqueued')
->dateTime()
->sortable(),
Tables\Columns\TextColumn::make('type')
->label('Type')
->formatStateUsing([OperationCatalog::class, 'label']),
Tables\Columns\TextColumn::make('status')
->badge()
->formatStateUsing(BadgeRenderer::label(BadgeDomain::OperationRunStatus))
->color(BadgeRenderer::color(BadgeDomain::OperationRunStatus))
->icon(BadgeRenderer::icon(BadgeDomain::OperationRunStatus))
->iconColor(BadgeRenderer::iconColor(BadgeDomain::OperationRunStatus)),
Tables\Columns\TextColumn::make('outcome')
->badge()
->formatStateUsing(BadgeRenderer::label(BadgeDomain::OperationRunOutcome))
->color(BadgeRenderer::color(BadgeDomain::OperationRunOutcome))
->icon(BadgeRenderer::icon(BadgeDomain::OperationRunOutcome))
->iconColor(BadgeRenderer::iconColor(BadgeDomain::OperationRunOutcome)),
Tables\Columns\TextColumn::make('counts')
->label('Counts')
->getStateUsing(function (OperationRun $record): string {
$counts = is_array($record->summary_counts) ? $record->summary_counts : [];
$total = (int) ($counts['total'] ?? 0);
$succeeded = (int) ($counts['succeeded'] ?? 0);
$failed = (int) ($counts['failed'] ?? 0);
if ($total === 0 && $succeeded === 0 && $failed === 0) {
return '—';
}
return sprintf('%d/%d (%d failed)', $succeeded, $total, $failed);
}),
])
->filters([])
->headerActions([])
->actions([
Actions\Action::make('view')
->label('View')
->icon('heroicon-o-eye')
->url(function (OperationRun $record): string {
$tenant = Tenant::currentOrFail();
return OperationRunLinks::view($record, $tenant);
})
->openUrlInNewTab(true),
])
->bulkActions([])
->emptyStateHeading('No schedule runs yet')
->emptyStateDescription('Operation history will appear here after this schedule has been enqueued.');
}
}