## Summary - standardize filter UX across key Filament resources with shared thin filter helpers for centralized option sourcing and archived/date-range presets - add persistence, essential filters, and OperationCatalog-aligned labels across the targeted resource tables - add and extend focused Pest coverage for guards, persistence, filter behavior, scope safety, and the new Spec 126 planning artifacts ## Spec 126 - add the full Spec 126 artifact set under `specs/126-filter-ux-standardization/` - align spec, plan, research, data model, quickstart, contract, checklist, and tasks for implementation readiness ## Validation - `vendor/bin/sail bin pint --dirty --format agent` - `vendor/bin/sail artisan test --compact tests/Feature/Guards/FilamentTableStandardsGuardTest.php tests/Feature/Filament/TableStatePersistenceTest.php tests/Feature/Findings/FindingsListFiltersTest.php tests/Feature/Findings/FindingsListDefaultsTest.php tests/Feature/Alerts/AlertDeliveryDeepLinkFiltersTest.php tests/Feature/Filament/Alerts/AlertDeliveryViewerTest.php tests/Feature/Filament/OperationRunListFiltersTest.php tests/Feature/Filament/PolicyVersionListFiltersTest.php tests/Feature/Filament/RestoreRunListFiltersTest.php tests/Feature/Filament/InventoryItemListFiltersTest.php tests/Feature/Filament/BaselineProfileListFiltersTest.php tests/Feature/ProviderConnections/TenantFilterOverrideTest.php tests/Feature/Rbac/InventoryItemResourceAuthorizationTest.php tests/Feature/Filament/BaselineTenantAssignmentsRelationManagerTest.php` ## Notes - no new OperationRun lifecycle or operational workflow behavior is introduced; only existing OperationRun table filter-label alignment and related coverage are in scope - existing authorization and action-surface semantics remain unchanged Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #154
234 lines
8.0 KiB
PHP
234 lines
8.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Resources\AlertDeliveryResource;
|
|
use App\Filament\Resources\AlertDeliveryResource\Pages\ListAlertDeliveries;
|
|
use App\Filament\Resources\AlertRuleResource;
|
|
use App\Models\AlertDelivery;
|
|
use App\Models\AlertDestination;
|
|
use App\Models\AlertRule;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Models\WorkspaceMembership;
|
|
use App\Services\Auth\WorkspaceCapabilityResolver;
|
|
use Filament\Actions\Action;
|
|
use Filament\Facades\Filament;
|
|
use Livewire\Features\SupportTesting\Testable;
|
|
use Livewire\Livewire;
|
|
|
|
function getAlertDeliveryEmptyStateAction(Testable $component, string $name): ?Action
|
|
{
|
|
foreach ($component->instance()->getTable()->getEmptyStateActions() as $action) {
|
|
if ($action instanceof Action && $action->getName() === $name) {
|
|
return $action;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function getAlertDeliveryHeaderAction(Testable $component, string $name): ?Action
|
|
{
|
|
$instance = $component->instance();
|
|
$instance->cacheInteractsWithHeaderActions();
|
|
|
|
foreach ($instance->getCachedHeaderActions() as $action) {
|
|
if ($action instanceof Action && $action->getName() === $name) {
|
|
return $action;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
it('lists only deliveries for entitled tenants', function (): void {
|
|
[$user, $tenantA] = createUserWithTenant(role: 'readonly');
|
|
|
|
$tenantB = Tenant::factory()->create([
|
|
'workspace_id' => (int) $tenantA->workspace_id,
|
|
]);
|
|
|
|
$workspaceId = (int) $tenantA->workspace_id;
|
|
|
|
$rule = AlertRule::factory()->create([
|
|
'workspace_id' => $workspaceId,
|
|
]);
|
|
|
|
$destination = AlertDestination::factory()->create([
|
|
'workspace_id' => $workspaceId,
|
|
]);
|
|
|
|
$tenantADelivery = AlertDelivery::factory()->create([
|
|
'workspace_id' => $workspaceId,
|
|
'tenant_id' => (int) $tenantA->getKey(),
|
|
'alert_rule_id' => (int) $rule->getKey(),
|
|
'alert_destination_id' => (int) $destination->getKey(),
|
|
'event_type' => 'high_drift',
|
|
]);
|
|
|
|
$tenantBDelivery = AlertDelivery::factory()->create([
|
|
'workspace_id' => $workspaceId,
|
|
'tenant_id' => (int) $tenantB->getKey(),
|
|
'alert_rule_id' => (int) $rule->getKey(),
|
|
'alert_destination_id' => (int) $destination->getKey(),
|
|
'event_type' => 'compare_failed',
|
|
]);
|
|
|
|
$this->actingAs($user);
|
|
|
|
Livewire::test(ListAlertDeliveries::class)
|
|
->assertCanSeeTableRecords([$tenantADelivery])
|
|
->assertCanNotSeeTableRecords([$tenantBDelivery]);
|
|
});
|
|
|
|
it('shows a guided empty state on alert deliveries and links to alert rules', function (): void {
|
|
[$user] = createUserWithTenant(role: 'owner');
|
|
|
|
$this->actingAs($user);
|
|
|
|
$component = Livewire::test(ListAlertDeliveries::class)
|
|
->assertTableEmptyStateActionsExistInOrder(['view_alert_rules'])
|
|
->assertSee('No alert deliveries')
|
|
->assertSee('Deliveries appear automatically when alert rules fire.');
|
|
|
|
$table = $component->instance()->getTable();
|
|
|
|
expect($table->getEmptyStateHeading())->toBe('No alert deliveries');
|
|
expect($table->getEmptyStateDescription())->toBe('Deliveries appear automatically when alert rules fire.');
|
|
expect($table->getEmptyStateIcon())->toBe('heroicon-o-bell-alert');
|
|
|
|
$action = getAlertDeliveryEmptyStateAction($component, 'view_alert_rules');
|
|
|
|
expect($action)->not->toBeNull();
|
|
expect($action?->getLabel())->toBe('View alert rules');
|
|
expect($action?->getUrl())->toBe(AlertRuleResource::getUrl(panel: 'admin'));
|
|
expect(getAlertDeliveryHeaderAction($component, 'view_alert_rules'))->toBeNull();
|
|
});
|
|
|
|
it('keeps alert deliveries header-action free after records exist', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$workspaceId = (int) $tenant->workspace_id;
|
|
|
|
$rule = AlertRule::factory()->create([
|
|
'workspace_id' => $workspaceId,
|
|
]);
|
|
|
|
$destination = AlertDestination::factory()->create([
|
|
'workspace_id' => $workspaceId,
|
|
]);
|
|
|
|
$delivery = AlertDelivery::factory()->create([
|
|
'workspace_id' => $workspaceId,
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'alert_rule_id' => (int) $rule->getKey(),
|
|
'alert_destination_id' => (int) $destination->getKey(),
|
|
]);
|
|
|
|
$this->actingAs($user);
|
|
|
|
$component = Livewire::test(ListAlertDeliveries::class)
|
|
->assertCanSeeTableRecords([$delivery]);
|
|
|
|
expect(getAlertDeliveryHeaderAction($component, 'view_alert_rules'))->toBeNull();
|
|
});
|
|
|
|
it('returns 404 when a member from another workspace tries to view a delivery', function (): void {
|
|
[$user] = createUserWithTenant(role: 'owner');
|
|
|
|
$otherWorkspace = Workspace::factory()->create();
|
|
$tenant = Tenant::factory()->create([
|
|
'workspace_id' => (int) $otherWorkspace->getKey(),
|
|
]);
|
|
$rule = AlertRule::factory()->create([
|
|
'workspace_id' => (int) $otherWorkspace->getKey(),
|
|
]);
|
|
$destination = AlertDestination::factory()->create([
|
|
'workspace_id' => (int) $otherWorkspace->getKey(),
|
|
]);
|
|
$delivery = AlertDelivery::factory()->create([
|
|
'workspace_id' => (int) $otherWorkspace->getKey(),
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'alert_rule_id' => (int) $rule->getKey(),
|
|
'alert_destination_id' => (int) $destination->getKey(),
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get(AlertDeliveryResource::getUrl('view', ['record' => $delivery], panel: 'admin'))
|
|
->assertNotFound();
|
|
});
|
|
|
|
it('returns 403 for members missing alerts view capability on deliveries index', function (): void {
|
|
$workspace = Workspace::factory()->create();
|
|
$user = User::factory()->create();
|
|
|
|
WorkspaceMembership::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'user_id' => (int) $user->getKey(),
|
|
'role' => 'readonly',
|
|
]);
|
|
|
|
$resolver = \Mockery::mock(WorkspaceCapabilityResolver::class);
|
|
$resolver->shouldReceive('isMember')->andReturnTrue();
|
|
$resolver->shouldReceive('can')->andReturnFalse();
|
|
app()->instance(WorkspaceCapabilityResolver::class, $resolver);
|
|
|
|
session()->put(\App\Support\Workspaces\WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
|
|
|
|
$this->actingAs($user)
|
|
->get(AlertDeliveryResource::getUrl(panel: 'admin'))
|
|
->assertForbidden();
|
|
});
|
|
|
|
it('keeps persisted alert delivery filters inside the active tenant scope', function (): void {
|
|
$tenantA = Tenant::factory()->create();
|
|
[$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'owner');
|
|
|
|
$tenantB = Tenant::factory()->create([
|
|
'workspace_id' => (int) $tenantA->workspace_id,
|
|
]);
|
|
|
|
createUserWithTenant(tenant: $tenantB, user: $user, role: 'owner');
|
|
|
|
$workspaceId = (int) $tenantA->workspace_id;
|
|
|
|
$rule = AlertRule::factory()->create([
|
|
'workspace_id' => $workspaceId,
|
|
]);
|
|
|
|
$destination = AlertDestination::factory()->create([
|
|
'workspace_id' => $workspaceId,
|
|
]);
|
|
|
|
$tenantADelivery = AlertDelivery::factory()->create([
|
|
'workspace_id' => $workspaceId,
|
|
'tenant_id' => (int) $tenantA->getKey(),
|
|
'alert_rule_id' => (int) $rule->getKey(),
|
|
'alert_destination_id' => (int) $destination->getKey(),
|
|
'status' => AlertDelivery::STATUS_SENT,
|
|
]);
|
|
|
|
$tenantBDelivery = AlertDelivery::factory()->create([
|
|
'workspace_id' => $workspaceId,
|
|
'tenant_id' => (int) $tenantB->getKey(),
|
|
'alert_rule_id' => (int) $rule->getKey(),
|
|
'alert_destination_id' => (int) $destination->getKey(),
|
|
'status' => AlertDelivery::STATUS_SENT,
|
|
]);
|
|
|
|
$this->actingAs($user);
|
|
Filament::setTenant($tenantA, true);
|
|
|
|
Livewire::test(ListAlertDeliveries::class)
|
|
->filterTable('status', AlertDelivery::STATUS_SENT)
|
|
->assertCanSeeTableRecords([$tenantADelivery])
|
|
->assertCanNotSeeTableRecords([$tenantBDelivery]);
|
|
|
|
Livewire::test(ListAlertDeliveries::class)
|
|
->assertSet('tableFilters.status.value', AlertDelivery::STATUS_SENT)
|
|
->assertCanSeeTableRecords([$tenantADelivery])
|
|
->assertCanNotSeeTableRecords([$tenantBDelivery]);
|
|
});
|