## Summary - replace the static Inventory Coverage HTML tables with a Filament native searchable, sortable, filterable table on the existing tenant page - normalize supported policy types and foundations into one runtime dataset while preserving centralized badge semantics and the documented read-only action-surface exemption - add the full spec kit artifact set for feature 124 and focused Pest coverage for rendering, search, sort, filters, empty state, and regression-sensitive page copy ## Testing - `vendor/bin/sail bin pint --dirty --format agent` - `vendor/bin/sail artisan test --compact tests/Feature/Filament/InventoryCoverageTableTest.php tests/Feature/Filament/InventoryPagesTest.php tests/Feature/Filament/InventoryHubDbOnlyTest.php` ## Filament Notes - Livewire v4.0+ compliance: yes, this uses Filament v5 table APIs on the existing page and does not introduce any Livewire v3 patterns - Provider registration: unchanged; Laravel 11+ provider registration remains in `bootstrap/providers.php` - Globally searchable resources: none changed in this feature; no Resource global-search behavior was added or modified - Destructive actions: none; the page remains read-only and only exposes a non-destructive clear-filters empty-state action - Asset strategy: no new panel or shared assets were added, so no `filament:assets` deployment change is required for this feature - Testing plan delivered: focused Filament/Pest coverage for the page table surface plus existing page-load regressions ## Follow-up - Manual dark-mode and badge-regression QA from task `T018` is still pending and should be completed before merge if that check remains mandatory in your review flow. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #151
216 lines
9.7 KiB
PHP
216 lines
9.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Pages\InventoryCoverage;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Support\Badges\BadgeCatalog;
|
|
use App\Support\Badges\BadgeDomain;
|
|
use App\Support\Badges\TagBadgeCatalog;
|
|
use App\Support\Badges\TagBadgeDomain;
|
|
use Filament\Facades\Filament;
|
|
use Filament\Tables\Columns\IconColumn;
|
|
use Filament\Tables\Columns\TextColumn;
|
|
use Livewire\Features\SupportTesting\Testable;
|
|
use Livewire\Livewire;
|
|
|
|
function inventoryCoverageRecordKey(string $segment, string $type): string
|
|
{
|
|
return "{$segment}:{$type}";
|
|
}
|
|
|
|
function inventoryCoverageComponent(User $user, Tenant $tenant): Testable
|
|
{
|
|
$tenant->makeCurrent();
|
|
Filament::setTenant($tenant, true);
|
|
|
|
test()->actingAs($user);
|
|
|
|
return Livewire::actingAs($user)->test(InventoryCoverage::class);
|
|
}
|
|
|
|
function removeInventoryCoverageRestoreMetadata(): void
|
|
{
|
|
config()->set(
|
|
'tenantpilot.supported_policy_types',
|
|
collect(config('tenantpilot.supported_policy_types', []))
|
|
->map(function (array $row): array {
|
|
unset($row['restore']);
|
|
|
|
return $row;
|
|
})
|
|
->all(),
|
|
);
|
|
|
|
config()->set(
|
|
'tenantpilot.foundation_types',
|
|
collect(config('tenantpilot.foundation_types', []))
|
|
->map(function (array $row): array {
|
|
unset($row['restore']);
|
|
|
|
return $row;
|
|
})
|
|
->all(),
|
|
);
|
|
}
|
|
|
|
it('renders searchable coverage rows for policy and foundation metadata', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$conditionalAccessKey = inventoryCoverageRecordKey('policy', 'conditionalAccessPolicy');
|
|
$deviceConfigurationKey = inventoryCoverageRecordKey('policy', 'deviceConfiguration');
|
|
$scopeTagKey = inventoryCoverageRecordKey('foundation', 'roleScopeTag');
|
|
|
|
inventoryCoverageComponent($user, $tenant)
|
|
->assertOk()
|
|
->assertTableColumnExists('type')
|
|
->assertTableColumnExists('label')
|
|
->assertTableColumnExists('category')
|
|
->assertTableColumnExists('dependencies')
|
|
->assertCountTableRecords(
|
|
count(config('tenantpilot.supported_policy_types', [])) + count(config('tenantpilot.foundation_types', [])),
|
|
)
|
|
->assertCanSeeTableRecords([$conditionalAccessKey, $scopeTagKey])
|
|
->searchTable('conditional')
|
|
->assertCanSeeTableRecords([$conditionalAccessKey])
|
|
->assertCanNotSeeTableRecords([$deviceConfigurationKey, $scopeTagKey])
|
|
->searchTable('Scope Tag')
|
|
->assertCanSeeTableRecords([$scopeTagKey])
|
|
->assertCanNotSeeTableRecords([$conditionalAccessKey])
|
|
->searchTable(null)
|
|
->assertCanSeeTableRecords([$conditionalAccessKey, $deviceConfigurationKey, $scopeTagKey]);
|
|
});
|
|
|
|
it('sorts coverage rows by type and label deterministically', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$appProtectionKey = inventoryCoverageRecordKey('policy', 'appProtectionPolicy');
|
|
$conditionalAccessKey = inventoryCoverageRecordKey('policy', 'conditionalAccessPolicy');
|
|
$deviceComplianceKey = inventoryCoverageRecordKey('policy', 'deviceCompliancePolicy');
|
|
$adminTemplatesKey = inventoryCoverageRecordKey('policy', 'groupPolicyConfiguration');
|
|
$appConfigDeviceKey = inventoryCoverageRecordKey('policy', 'managedDeviceAppConfiguration');
|
|
$appConfigMamKey = inventoryCoverageRecordKey('policy', 'mamAppConfiguration');
|
|
|
|
inventoryCoverageComponent($user, $tenant)
|
|
->sortTable('type')
|
|
->assertCanSeeTableRecords([$appProtectionKey, $conditionalAccessKey, $deviceComplianceKey], inOrder: true)
|
|
->sortTable('label')
|
|
->assertCanSeeTableRecords([$adminTemplatesKey, $appConfigDeviceKey, $appConfigMamKey], inOrder: true);
|
|
});
|
|
|
|
it('filters coverage rows by category and restore mode when restore metadata exists', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$assignmentFilterKey = inventoryCoverageRecordKey('foundation', 'assignmentFilter');
|
|
$scopeTagKey = inventoryCoverageRecordKey('foundation', 'roleScopeTag');
|
|
$deviceConfigurationKey = inventoryCoverageRecordKey('policy', 'deviceConfiguration');
|
|
$conditionalAccessKey = inventoryCoverageRecordKey('policy', 'conditionalAccessPolicy');
|
|
$securityBaselineKey = inventoryCoverageRecordKey('policy', 'securityBaselinePolicy');
|
|
|
|
inventoryCoverageComponent($user, $tenant)
|
|
->assertTableFilterExists('category')
|
|
->assertTableFilterExists('restore')
|
|
->filterTable('category', 'Foundations')
|
|
->assertCanSeeTableRecords([$assignmentFilterKey, $scopeTagKey])
|
|
->assertCanNotSeeTableRecords([$deviceConfigurationKey, $conditionalAccessKey])
|
|
->removeTableFilters()
|
|
->filterTable('restore', 'preview-only')
|
|
->assertCanSeeTableRecords([$conditionalAccessKey, $securityBaselineKey])
|
|
->assertCanNotSeeTableRecords([$deviceConfigurationKey, $assignmentFilterKey]);
|
|
});
|
|
|
|
it('omits the restore filter when the runtime dataset has no restore metadata', function (): void {
|
|
removeInventoryCoverageRestoreMetadata();
|
|
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$component = inventoryCoverageComponent($user, $tenant)
|
|
->assertTableFilterExists('category');
|
|
|
|
expect($component->instance()->getTable()->getFilter('restore'))->toBeNull();
|
|
});
|
|
|
|
it('shows a single clear-filters empty state action and can reset back to a populated dataset', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$conditionalAccessKey = inventoryCoverageRecordKey('policy', 'conditionalAccessPolicy');
|
|
|
|
inventoryCoverageComponent($user, $tenant)
|
|
->assertTableEmptyStateActionsExistInOrder(['clear_filters'])
|
|
->searchTable('no-such-coverage-entry')
|
|
->assertCountTableRecords(0)
|
|
->assertSee('No coverage entries match this view')
|
|
->assertSee('Clear filters')
|
|
->searchTable(null)
|
|
->assertCountTableRecords(
|
|
count(config('tenantpilot.supported_policy_types', [])) + count(config('tenantpilot.foundation_types', [])),
|
|
)
|
|
->assertCanSeeTableRecords([$conditionalAccessKey]);
|
|
});
|
|
|
|
it('preserves badge semantics and dependency indicators in the interactive table columns', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$conditionalAccessKey = inventoryCoverageRecordKey('policy', 'conditionalAccessPolicy');
|
|
$deviceConfigurationKey = inventoryCoverageRecordKey('policy', 'deviceConfiguration');
|
|
$assignmentFilterKey = inventoryCoverageRecordKey('foundation', 'assignmentFilter');
|
|
|
|
$typeSpec = TagBadgeCatalog::spec(TagBadgeDomain::PolicyType, 'conditionalAccessPolicy');
|
|
$categorySpec = TagBadgeCatalog::spec(TagBadgeDomain::PolicyCategory, 'Conditional Access');
|
|
$restoreSpec = BadgeCatalog::spec(BadgeDomain::PolicyRestoreMode, 'preview-only');
|
|
$riskSpec = BadgeCatalog::spec(BadgeDomain::PolicyRisk, 'high');
|
|
|
|
inventoryCoverageComponent($user, $tenant)
|
|
->assertTableColumnFormattedStateSet('label', $typeSpec->label, $conditionalAccessKey)
|
|
->assertTableColumnFormattedStateSet('category', $categorySpec->label, $conditionalAccessKey)
|
|
->assertTableColumnFormattedStateSet('restore', $restoreSpec->label, $conditionalAccessKey)
|
|
->assertTableColumnFormattedStateSet('risk', $riskSpec->label, $conditionalAccessKey)
|
|
->assertTableColumnFormattedStateSet('segment', 'Policy', $conditionalAccessKey)
|
|
->assertTableColumnFormattedStateSet('segment', 'Foundation', $assignmentFilterKey)
|
|
->assertTableColumnStateSet('dependencies', true, $deviceConfigurationKey)
|
|
->assertTableColumnStateSet('dependencies', false, $assignmentFilterKey)
|
|
->assertTableColumnExists('label', function (TextColumn $column) use ($typeSpec): bool {
|
|
$state = $column->getState();
|
|
|
|
return $column->isBadge()
|
|
&& $column->getColor($state) === $typeSpec->color
|
|
&& $column->getIcon($state) === $typeSpec->icon;
|
|
}, $conditionalAccessKey)
|
|
->assertTableColumnExists('category', function (TextColumn $column) use ($categorySpec): bool {
|
|
$state = $column->getState();
|
|
|
|
return $column->isBadge()
|
|
&& $column->getColor($state) === $categorySpec->color
|
|
&& $column->getIcon($state) === $categorySpec->icon;
|
|
}, $conditionalAccessKey)
|
|
->assertTableColumnExists('restore', function (TextColumn $column) use ($restoreSpec): bool {
|
|
$state = $column->getState();
|
|
|
|
return $column->isBadge()
|
|
&& $column->getColor($state) === $restoreSpec->color
|
|
&& $column->getIcon($state) === $restoreSpec->icon;
|
|
}, $conditionalAccessKey)
|
|
->assertTableColumnExists('risk', function (TextColumn $column) use ($riskSpec): bool {
|
|
$state = $column->getState();
|
|
|
|
return $column->isBadge()
|
|
&& $column->getColor($state) === $riskSpec->color
|
|
&& $column->getIcon($state) === $riskSpec->icon;
|
|
}, $conditionalAccessKey)
|
|
->assertTableColumnExists('dependencies', function (IconColumn $column): bool {
|
|
$state = $column->getState();
|
|
|
|
return $state === true
|
|
&& $column->getColor($state) === 'success'
|
|
&& (string) $column->getIcon($state) === 'heroicon-m-check-circle';
|
|
}, $deviceConfigurationKey)
|
|
->assertTableColumnExists('dependencies', function (IconColumn $column): bool {
|
|
$state = $column->getState();
|
|
|
|
return $state === false
|
|
&& $column->getColor($state) === 'gray'
|
|
&& (string) $column->getIcon($state) === 'heroicon-m-minus-circle';
|
|
}, $assignmentFilterKey);
|
|
});
|