# TenantPilot — Comprehensive Filter Audit > **Stack:** Laravel 12 · Filament v5 · Livewire v4 · PostgreSQL > **Scope:** Every table surface across Admin Panel, System Console, Dashboard Widgets & Livewire Pickers > **Date:** 2025-07-11 --- ## 1. Executive Summary The platform has **36 table surfaces** across 18 Resources, 6 RelationManagers, 9 custom/system pages, 2 dashboard widgets, and 3 Livewire picker components. **Key findings:** | Metric | Value | |---|---| | Surfaces with ≥1 filter | 16 of 36 (44 %) | | Surfaces with zero filters | 20 of 36 (56 %) | | Total filter instances | ~54 | | Surfaces with `persistFiltersInSession()` | 7 of 36 (19 %) | | Surfaces with filter defaults | 6 | | Filter types used | `SelectFilter` (dominant), `Filter::make` (boolean), `TrashedFilter`, `TernaryFilter`, `DatePicker` range | | Highest filter count on one surface | 9 (FindingResource) | | Resources with zero filters that need them | 5+ (identified below) | **Critical inconsistencies:** - Session persistence is applied to only 7 of 16 filtered surfaces — the other 9 filtered surfaces lose user state on navigation. - Filter defaults vary wildly: some surfaces pre-filter to "active" records, others show everything. - No date range filter exists on surfaces that clearly need one (Findings, AlertDelivery, RestoreRun). - Status/enum filters use inconsistent option sets and labeling across the platform. - The `TrashedFilter` (Active/All/Archived) pattern is consistent where used but only applied to 4 resources. --- ## 2. Complete Surface Inventory ### 2A. Resources (18) | # | Resource | Domain | Scope | Filters | Persist? | Default | |---|---|---|---|---|---|---| | 1 | PolicyResource | Inventory | Tenant | 4 (visibility, policy_type, category, platform) | ✅ | visibility=active | | 2 | FindingResource | Governance | Tenant | 9 (open, overdue, high_severity, my_assigned, status, finding_type, evidence_fidelity, scope_key, run_ids) | ✅ | open=true | | 3 | OperationRunResource | Monitoring | Workspace | 6 (tenant_id, type, status, outcome, initiator_name, created_at) | ✅ | tenant=active, created_at=30d | | 4 | TenantResource | Settings | Workspace | 3 (trashed, environment, app_status) | ✅ | trashed=active | | 5 | BackupScheduleResource | Backups | Tenant | 2 (trashed, enabled_state) | ✅ | — | | 6 | BackupSetResource | Backups | Tenant | 1 (trashed) | ✅ | — | | 7 | ProviderConnectionResource | Settings | Workspace | 5 (tenant, provider, status, health_status, default_only) | ✅ | tenant=scoped | | 8 | PolicyVersionResource | Inventory | Tenant | 1 (trashed) | ❌ | — | | 9 | RestoreRunResource | Backups | Tenant | 1 (trashed) | ❌ | — | | 10 | InventoryItemResource | Inventory | Tenant | 2 (policy_type, category) | ❌ | — | | 11 | ReviewPackResource | Reporting | Tenant | 1 (status) | ❌ | — | | 12 | AlertDeliveryResource | Monitoring | Workspace | 3 (status, event_type, alert_destination_id) | ❌ | — | | 13 | EntraGroupResource | Directory | Tenant | 2 (stale, group_type) | ❌ | — | | 14 | AlertRuleResource | Monitoring | Workspace | 0 | ❌ | — | | 15 | AlertDestinationResource | Monitoring | Workspace | 0 | ❌ | — | | 16 | BaselineProfileResource | Governance | Workspace | 0 | ❌ | — | | 17 | BaselineSnapshotResource | Governance | Workspace | 0 | ❌ | — | | 18 | WorkspaceResource | Settings | Workspace | 0 | ❌ | — | ### 2B. RelationManagers (6) | # | RelationManager | Parent | Filters | Notes | |---|---|---|---|---| | 1 | VersionsRelationManager | PolicyResource | 0 (`->filters([])`) | Explicit empty | | 2 | BackupItemsRelationManager | BackupSetResource | 0 (`->filters([])`) | Explicit empty | | 3 | BackupScheduleOperationRunsRelationManager | BackupScheduleResource | 0 (`->filters([])`) | Explicit empty | | 4 | BaselineTenantAssignmentsRelationManager | BaselineProfileResource | 0 | No filters block | | 5 | TenantMembershipsRelationManager | TenantResource | 0 | No filters block | | 6 | WorkspaceMembershipsRelationManager | WorkspaceResource | 0 | No filters block | ### 2C. Custom & System Pages (9) | # | Page | Panel | Filters | Notes | |---|---|---|---|---| | 1 | InventoryCoverage | Admin | 1-2 (category, restore mode) | Dynamic — restore filter only if options exist | | 2 | Monitoring/Operations | Admin | Reuses OperationRunResource::table() | Inherits 6 filters + persistence | | 3 | System/Ops/Runs | System | 0 | No filters on platform ops listing | | 4 | System/Ops/Failures | System | 0 | Pre-filtered query (failed only), no user filters | | 5 | System/Ops/Stuck | System | 0 | Pre-filtered via StuckRunClassifier, no user filters | | 6 | System/Directory/Tenants | System | 0 | No filters | | 7 | System/Directory/Workspaces | System | 0 | No filters | | 8 | System/Security/AccessLogs | System | 0 | No filters | | 9 | System/RepairWorkspaceOwners | System | 0 | No filters | ### 2D. Dashboard Widgets (2) | # | Widget | Filters | Notes | |---|---|---|---| | 1 | RecentDriftFindings | 0 | Polling-based, no filter UI | | 2 | RecentOperations | 0 | Polling-based, no filter UI | ### 2E. Livewire Picker Components (3) | # | Component | Filters | Notes | |---|---|---|---| | 1 | BackupSetPolicyPickerTable | 5 (policy_type, platform, synced_within, ignored, has_versions) | Most filters of any picker; synced_within default=7, ignored default=false | | 2 | EntraGroupCachePickerTable | 2 (stale, group_type) | Mirrors EntraGroupResource filters | | 3 | SettingsCatalogSettingsTable | 0 | Read-only inspection table; purely search-driven | --- ## 3. Metrics Dashboard ### 3A. Filter Type Distribution | Filter Type | Count | Surfaces Using It | |---|---|---| | `SelectFilter` | ~35 | PolicyResource, FindingResource, OperationRunResource, TenantResource, BackupScheduleResource, ProviderConnectionResource, InventoryItemResource, ReviewPackResource, AlertDeliveryResource, EntraGroupResource, InventoryCoverage, BackupSetPolicyPicker, EntraGroupCachePicker | | `Filter::make` (boolean/custom) | ~8 | FindingResource (4: open, overdue, high_severity, my_assigned), ProviderConnectionResource (1: default_only), FindingResource (2: scope_key, run_ids) | | `TrashedFilter` | 4 | TenantResource, BackupScheduleResource, BackupSetResource, PolicyVersionResource, RestoreRunResource | | `TernaryFilter` | 1 | BackupSetPolicyPickerTable (ignored) | | Date range (`DatePicker`) | 1 | OperationRunResource (created_at) | ### 3B. Persistence Coverage | Category | With Persistence | Without | Total | |---|---|---|---| | Resources with filters | 7 | 6 | 13 | | RelationManagers | 0 | 6 | 6 | | Custom/System Pages | 1 (via reuse) | 8 | 9 | | Widgets | 0 | 2 | 2 | | Livewire Pickers | 0 | 3 | 3 | ### 3C. Default Filter Coverage | Surface | Filter | Default Value | Impact | |---|---|---|---| | PolicyResource | visibility | `active` | Hides ignored policies on first load | | FindingResource | open | `true` | Shows only open findings on first load | | OperationRunResource | tenant_id | Active tenant | Scopes to current tenant context | | OperationRunResource | created_at | Last 30 days | Time-bounds initial view | | TenantResource | trashed | `true` (Active) | Hides archived tenants | | BackupSetPolicyPickerTable | synced_within | `7` (7 days) | Only recently synced policies | | BackupSetPolicyPickerTable | ignored | `false` | Hides ignored policies | | ProviderConnectionResource | tenant | Scoped tenant external_id | Pre-scopes to current tenant | --- ## 4. Detailed Findings Per Table ### 4.1 PolicyResource **Filters (4):** ``` SelectFilter('visibility') → default('active'), custom query (active hides ignored_at) SelectFilter('policy_type') → from config, no searchable SelectFilter('category') → custom query mapping types to categories SelectFilter('platform') → from distinct DB values ``` **Implicit filter:** `modifyQueryUsing` hides policies not synced in 7 days. **Assessment:** - ✅ Persistence: full (search, sort, filters) - ✅ Default: sensible (filters to active policies) - ⚠️ `policy_type` options come from config — could be `->searchable()` for large lists - ⚠️ `platform` options use raw DB distinct — no label formatting - ⚠️ No date range filter (last_synced_at is sortable but not filterable) --- ### 4.2 FindingResource **Filters (9) — highest in the product:** ``` Filter::make('open') → boolean quick, default(true) Filter::make('overdue') → boolean quick Filter::make('high_severity') → boolean quick Filter::make('my_assigned') → boolean quick SelectFilter('status') → 8 manual options SelectFilter('finding_type') → 3 manual options SelectFilter('evidence_fidelity') → 2 manual options Filter::make('scope_key') → TextInput form Filter::make('run_ids') → 2 TextInputs (baseline + current run) ``` **Assessment:** - ✅ Persistence: full - ✅ Default: sensible (open=true) - ⚠️ 9 filters may overwhelm users — consider filter groups or collapsible sections - ⚠️ `scope_key` and `run_ids` are advanced/developer filters — should be in an "Advanced" section - ⚠️ No date range filter (created_at, due_at exist but are not filterable) - ⚠️ `status` has 8 manual options — should use enum for consistency --- ### 4.3 OperationRunResource **Filters (6):** ``` SelectFilter('tenant_id') → dynamic default=active tenant, relationship options, searchable SelectFilter('type') → from distinct DB values SelectFilter('status') → from OperationRunStatus enum SelectFilter('outcome') → from OperationRunOutcome enum with labels SelectFilter('initiator_name') → from distinct DB, searchable Filter::make('created_at') → DatePicker from/until, default=last 30 days ``` **Assessment:** - ✅ Persistence: full - ✅ Defaults: excellent (tenant + date range scoped) - ✅ Best filter UX in the product — good model for others - ⚠️ `type` uses raw DB distinct instead of OperationCatalog labels - ✅ Date range filter — only surface that has one --- ### 4.4 TenantResource **Filters (3):** ``` TrashedFilter::make() → labels (Active/All/Archived), default(true) SelectFilter('environment') → 4 options SelectFilter('app_status') → 4 options ``` **Assessment:** - ✅ Persistence: full - ✅ TrashedFilter pattern with default - ✅ Good but minimal --- ### 4.5 BackupScheduleResource **Filters (2):** ``` TrashedFilter::make() → Active/All/Archived SelectFilter('enabled_state') → custom query ``` **Assessment:** - ✅ Persistence: full - ⚠️ Only 2 filters for a moderately complex resource --- ### 4.6 BackupSetResource **Filters (1):** ``` TrashedFilter::make() → Active/All/Archived ``` **Assessment:** - ✅ Persistence: full - ⚠️ Minimal filtering — no status, no date range, no policy type summary filter --- ### 4.7 ProviderConnectionResource **Filters (5):** ``` SelectFilter('tenant') → default=scoped tenant external_id, custom query SelectFilter('provider') → 1 option (microsoft) SelectFilter('status') → 4 options SelectFilter('health_status') → 4 options Filter::make('default_only') → boolean ``` **Assessment:** - ✅ Persistence: full - ✅ Good filter coverage for the domain - ⚠️ `provider` filter has only 1 option — may be premature; could be hidden until >1 provider exists --- ### 4.8 PolicyVersionResource **Filters (1):** ``` TrashedFilter::make() → Active/All/Archived ``` **Assessment:** - ❌ No persistence - ⚠️ Missing: policy_type, platform, date range (captured_at) - ⚠️ Gap: this is a high-traffic resource with version history — more filters needed --- ### 4.9 RestoreRunResource **Filters (1):** ``` TrashedFilter::make() → Active/All/Archived ``` **Assessment:** - ❌ No persistence - ⚠️ Missing: status, outcome, date range (started_at), dry_run flag - ⚠️ Gap: operators need to quickly find failed or dry-run restores --- ### 4.10 InventoryItemResource **Filters (2):** ``` SelectFilter('policy_type') → searchable SelectFilter('category') → searchable ``` **Assessment:** - ❌ **No persistence** — critical gap since this is a frequently navigated list - ⚠️ Missing: platform, sync freshness, version count - ⚠️ Both filters are searchable — good, but should match PolicyResource pattern --- ### 4.11 ReviewPackResource **Filters (1):** ``` SelectFilter('status') → from ReviewPackStatus enum ``` **Assessment:** - ❌ No persistence - ⚠️ Single filter is reasonable for current scope --- ### 4.12 AlertDeliveryResource **Filters (3):** ``` SelectFilter('status') → 6 options SelectFilter('event_type') → dynamic from DB SelectFilter('alert_destination_id') → dynamic from DB ``` **Assessment:** - ❌ No persistence - ⚠️ Missing: date range (created_at), tenant filter if multi-tenant scope - ⚠️ Status options are manual strings — should use enum --- ### 4.13 EntraGroupResource **Filters (2):** ``` SelectFilter('stale') → config-based staleness cutoff query SelectFilter('group_type') → complex JSON/boolean query ``` **Assessment:** - ❌ No persistence - ⚠️ Complex custom queries — well-implemented but should document the staleness logic - ⚠️ Mirrors EntraGroupCachePickerTable filters — good consistency --- ### 4.14–4.18 Resources with ZERO filters | Resource | Assessment | |---|---| | **AlertRuleResource** | ⚠️ Missing: is_active toggle, event type, severity | | **AlertDestinationResource** | ⚠️ Missing: type/channel filter | | **BaselineProfileResource** | ⚠️ Missing: status filter | | **BaselineSnapshotResource** | ⚠️ Missing: state filter (gaps/complete) | | **WorkspaceResource** | ✅ Acceptable — small dataset, search suffices | --- ## 5. Cross-Cutting Inconsistencies ### 5.1 Persistence Gap **7 resources** have persistence. **11 resources + all 6 relation managers + 8 system pages** do not. The guard test (`FilamentTableStandardsGuardTest`) only enforces persistence on 7 "critical" resources: - TenantResource, PolicyResource, BackupSetResource, BackupScheduleResource - ProviderConnectionResource, FindingResource, OperationRunResource **Missing from guard:** PolicyVersionResource, RestoreRunResource, InventoryItemResource, AlertDeliveryResource, EntraGroupResource, ReviewPackResource. ### 5.2 Filter Default Inconsistency | Pattern | Active-by-default Behavior | Surfaces | |---|---|---| | `SelectFilter('visibility')->default('active')` | Custom query | PolicyResource | | `Filter::make('open')->default()` | Boolean toggle | FindingResource | | `TrashedFilter->default(true)` | Hides archived | TenantResource | | `SelectFilter('tenant_id')->default(...)` | Scopes to context | OperationRunResource | | No default | Shows everything | 10+ surfaces | **Problem:** Users expect consistent behavior across similar resources. Some show "active" records by default, some show everything. ### 5.3 Status Filter Inconsistency Status filtering is implemented differently across surfaces: | Surface | Approach | |---|---| | OperationRunResource | Enum → `OperationRunStatus::class` | | FindingResource | Manual array of 8 strings | | AlertDeliveryResource | Manual array of 6 strings | | ReviewPackResource | Enum → `ReviewPackStatus::class` | | PolicyVersion/RestoreRun | TrashedFilter (soft-delete based) | **Recommendation:** Always source status options from the model's status enum or a centralized catalog. ### 5.4 Date Range Filter Gap Only **OperationRunResource** has a date range filter (`created_at` with DatePicker form). Surfaces that should have one but don't: - **FindingResource** — has `created_at`, `due_at` - **AlertDeliveryResource** — has `created_at` - **RestoreRunResource** — has `started_at`, `completed_at` - **PolicyVersionResource** — has `captured_at` - **BackupScheduleResource** — has execution history ### 5.5 TrashedFilter Labeling All 4 resources using TrashedFilter apply the same label pattern — **this is consistent ✅:** ```php TrashedFilter::make() ->label('Archived') ->placeholder('Active') ->trueLabel('All') ->falseLabel('Archived') ``` ### 5.6 Tenant Scoping in Filters | Surface | Approach | |---|---| | OperationRunResource | Explicit `SelectFilter('tenant_id')` with dynamic default | | ProviderConnectionResource | Explicit `SelectFilter('tenant')` with external_id default | | AlertDeliveryResource | ❌ No tenant filter (scoped in query only) | | System pages | ❌ No filters at all (but cross-tenant by design) | --- ## 6. Repeated Patterns (Extractable) ### Pattern A: TrashedFilter Standard ```php TrashedFilter::make() ->label('Archived') ->placeholder('Active') ->trueLabel('All') ->falseLabel('Archived') ``` **Used in:** TenantResource, BackupScheduleResource, BackupSetResource, PolicyVersionResource, RestoreRunResource **Extraction:** Could be a helper `FilterPresets::trashedArchive()`. ### Pattern B: Config-Sourced SelectFilter ```php SelectFilter::make('policy_type') ->label('Policy type') ->options($options) // from config or DB ->searchable() ``` **Used in:** PolicyResource, InventoryItemResource, BackupSetPolicyPickerTable **Note:** Options source varies (config vs DB distinct vs static array). ### Pattern C: Dynamic DB Distinct Options ```php SelectFilter::make('platform') ->options(fn (): array => Policy::query() ->where(...) ->whereNotNull('platform') ->distinct() ->orderBy('platform') ->pluck('platform', 'platform') ->all()) ``` **Used in:** PolicyResource (platform), OperationRunResource (type, initiator_name), BackupSetPolicyPickerTable (platform), AlertDeliveryResource (event_type, alert_destination_id) **Risk:** N+1 on every filter render — should cache or use `->options()` with static method. ### Pattern D: Date Range with DatePicker (ONLY ONE INSTANCE) ```php Filter::make('created_at') ->form([ Forms\Components\DatePicker::make('from')->label('From'), Forms\Components\DatePicker::make('until')->label('Until'), ]) ->query(...) ->indicateUsing(...) ``` **Used only in:** OperationRunResource **Should be in:** FindingResource, AlertDeliveryResource, RestoreRunResource, PolicyVersionResource ### Pattern E: Boolean Quick Filters ```php Filter::make('open') ->query(fn (Builder $query): Builder => $query->where('status', 'open')) ->default() ``` **Used in:** FindingResource (4 boolean quick filters) **Note:** Filament v5's `ToggleFilter` or `TernaryFilter` may provide better UX than `Filter::make` with `->default()`. ### Pattern F: Staleness Cutoff Filter ```php SelectFilter::make('stale') ->options(['1' => 'Stale', '0' => 'Fresh']) ->query(function (Builder $query, array $data) use ($cutoff): Builder { ... }) ``` **Used in:** EntraGroupResource, EntraGroupCachePickerTable (**consistent ✅**) --- ## 7. Recommended Filter UX Standard ### 7.1 Tier System | Tier | Surface Type | Filter Requirements | |---|---|---| | **Tier 1 — Critical** | High-traffic Resource lists (Policy, Finding, OperationRun, Tenant, InventoryItem) | Full persistence, smart defaults, date range where applicable | | **Tier 2 — Important** | Moderate-traffic Resources (BackupSchedule, BackupSet, RestoreRun, PolicyVersion, ProviderConnection, AlertDelivery, EntraGroup) | Persistence, domain-appropriate filters, defaults for soft-delete | | **Tier 3 — Standard** | Low-traffic Resources (ReviewPack, AlertRule, AlertDestination, BaselineProfile, BaselineSnapshot, Workspace) | Optional persistence, basic status filter if applicable | | **Tier 4 — Embedded** | RelationManagers, Widgets, System pages | No persistence needed, filters only if >50 typical records | | **Tier 5 — Pickers** | Modal picker tables | Task-specific filters, smart defaults to reduce noise | ### 7.2 Mandatory Filter Rules 1. **Persistence:** All Tier 1 and Tier 2 surfaces MUST have `->persistFiltersInSession()`, `->persistSearchInSession()`, `->persistSortInSession()`. 2. **Soft-delete:** Every resource with `SoftDeletes` MUST have the standard `TrashedFilter` with Active/All/Archived labels. 3. **Status enums:** All status filters MUST source options from the model's enum — no manual string arrays. 4. **Date range:** Surfaces with time-series data (created_at, captured_at, started_at) in Tier 1–2 SHOULD have a date range filter. 5. **Defaults:** Surfaces that commonly have "inactive" records (ignored, archived, completed) SHOULD default to showing only "active" records. ### 7.3 Filter Catalog (Extractable Presets) ```php // Proposed: App\Support\Filament\FilterPresets class FilterPresets { public static function trashedArchive(): TrashedFilter; public static function dateRange(string $column, ?int $defaultDays = null): Filter; public static function policyType(?int $tenantId = null): SelectFilter; public static function platform(?int $tenantId = null): SelectFilter; public static function tenantScope(): SelectFilter; } ``` --- ## 8. Filament-Native Feasibility | Pattern | Filament v5 Native? | Notes | |---|---|---| | `SelectFilter` | ✅ | Core, well-supported | | `TrashedFilter` | ✅ | Built-in, perfect for soft-delete | | `TernaryFilter` | ✅ | Used once (BackupSetPolicyPicker); underutilized | | `Filter::make` + `->default()` | ✅ | Works but boolean toggles are better as `TernaryFilter` | | `Filter::make` + `DatePicker` form | ✅ | Native pattern | | `->persistFiltersInSession()` | ✅ | Just needs to be added | | `->indicateUsing()` | ✅ | Critical UX — shows active filter badges | | Filter groups / sections | ⚠️ | Not natively supported; would need layout-level workaround | | FilterPresets helper | Custom | Thin wrapper — not against Filament philosophy | | Filter guards (tests) | Custom | Already done via `FilamentTableStandardsGuardTest` | **Verdict:** 100% achievable with Filament-native APIs. The only custom code needed is a thin `FilterPresets` helper for DRY and a guard test expansion. --- ## 9. Prioritized Gap List & Refactor Plan ### P0 — Critical (Session + Data Integrity) | # | Surface | Action | Effort | |---|---|---|---| | 1 | InventoryItemResource | Add `persistFiltersInSession()` + `persistSearchInSession()` + `persistSortInSession()` | XS | | 2 | PolicyVersionResource | Add full persistence trio | XS | | 3 | RestoreRunResource | Add full persistence trio | XS | | 4 | AlertDeliveryResource | Add full persistence trio | XS | | 5 | EntraGroupResource | Add full persistence trio | XS | | 6 | Guard test | Expand persistence enforcement list to include all Tier 1–2 resources | S | ### P1 — High (Missing Essential Filters) | # | Surface | Action | Effort | |---|---|---|---| | 7 | RestoreRunResource | Add status, outcome, date range filters | S | | 8 | PolicyVersionResource | Add policy_type, platform, date range (captured_at) filters | S | | 9 | FindingResource | Add date range filter (created_at/due_at) | S | | 10 | AlertDeliveryResource | Add date range filter (created_at) | S | | 11 | InventoryItemResource | Add platform, sync freshness filters | S | | 12 | BaselineProfileResource | Add status filter | XS | ### P2 — Medium (Consistency & UX Polish) | # | Surface | Action | Effort | |---|---|---|---| | 13 | FindingResource status | Refactor from manual strings to enum options | XS | | 14 | AlertDeliveryResource status | Refactor from manual strings to enum options | XS | | 15 | OperationRunResource type | Use OperationCatalog labels instead of raw DB strings | XS | | 16 | FilterPresets helper | Extract TrashedFilter, dateRange, policyType presets | S | | 17 | ProviderConnectionResource | Consider hiding provider filter until >1 provider exists | XS | | 18 | AlertRuleResource | Add is_active, event_type filters | S | | 19 | System/Ops/Runs | Add basic type/status/workspace filters | M | | 20 | System/Directory/Tenants | Add workspace, status filters | S | ### P3 — Low (Nice-to-Have) | # | Surface | Action | Effort | |---|---|---|---| | 21 | FindingResource | Move scope_key and run_ids filters to "Advanced" section | S | | 22 | BackupSetResource | Add backup frequency or item count filter | S | | 23 | BaselineSnapshotResource | Add state filter (has_gaps/complete) | S | | 24 | System/Security/AccessLogs | Add status and date range filters | S | | 25 | `->indicateUsing()` | Add active filter indicators to all date range filters | S | --- ## 10. Test Strategy ### Existing Coverage - `FilamentTableStandardsGuardTest` enforces: defaultSort, emptyStateHeading, persistence (7 resources), TablePaginationProfiles usage. ### Recommended Extensions **10.1 Expand Persistence Guard** Add all Tier 1–2 resources to the persistence enforcement list. **10.2 Filter Consistency Guard** ```php it('uses enum-based options for status filters', function () { // Scan all files for SelectFilter('status') and verify they use enum classes }); it('applies TrashedFilter on all soft-deletable resource tables', function () { // Cross-reference models with SoftDeletes trait against resource filter lists }); ``` **10.3 Filter Default Guard** ```php it('applies smart defaults on tier-1 resource filters', function () { // Verify PolicyResource, FindingResource, OperationRunResource have defaults }); ``` **10.4 Functional Filter Tests** For each Tier 1–2 surface, test: - Filter applies correct query scope - Filter default shows expected subset - Filter clears fully when reset - Multiple filters compose correctly --- ## 11. Spec Recommendations ### Proposed Spec: `XXX-filter-ux-standardization` **Scope:** Establish and enforce a repo-wide filter UX standard. **Deliverables:** 1. `FilterPresets` helper class with extractable patterns 2. Persistence trio added to all Tier 1–2 surfaces 3. Date range filters added to 5 time-series surfaces 4. Status filters migrated to enum-based options 5. Guard test expanded with filter-specific assertions 6. TrashedFilter standardized across all soft-deletable resources **Dependencies:** - Builds on completed `125-table-ux-standardization` spec - No breaking changes — purely additive **Estimated effort:** ~3–4 working sessions --- ## Appendix A: Full Filter Inventory Matrix | Surface | SelectFilter | Filter::make | TrashedFilter | TernaryFilter | DatePicker | Total | Persist | Defaults | |---|---|---|---|---|---|---|---|---| | PolicyResource | 4 | 0 | 0 | 0 | 0 | 4 | ✅ | 1 | | FindingResource | 3 | 6 | 0 | 0 | 0 | 9 | ✅ | 1 | | OperationRunResource | 5 | 1 | 0 | 0 | 1 | 6 | ✅ | 2 | | TenantResource | 2 | 0 | 1 | 0 | 0 | 3 | ✅ | 1 | | BackupScheduleResource | 1 | 0 | 1 | 0 | 0 | 2 | ✅ | 0 | | BackupSetResource | 0 | 0 | 1 | 0 | 0 | 1 | ✅ | 0 | | ProviderConnectionResource | 4 | 1 | 0 | 0 | 0 | 5 | ✅ | 1 | | PolicyVersionResource | 0 | 0 | 1 | 0 | 0 | 1 | ❌ | 0 | | RestoreRunResource | 0 | 0 | 1 | 0 | 0 | 1 | ❌ | 0 | | InventoryItemResource | 2 | 0 | 0 | 0 | 0 | 2 | ❌ | 0 | | ReviewPackResource | 1 | 0 | 0 | 0 | 0 | 1 | ❌ | 0 | | AlertDeliveryResource | 3 | 0 | 0 | 0 | 0 | 3 | ❌ | 0 | | EntraGroupResource | 2 | 0 | 0 | 0 | 0 | 2 | ❌ | 0 | | AlertRuleResource | 0 | 0 | 0 | 0 | 0 | 0 | ❌ | 0 | | AlertDestinationResource | 0 | 0 | 0 | 0 | 0 | 0 | ❌ | 0 | | BaselineProfileResource | 0 | 0 | 0 | 0 | 0 | 0 | ❌ | 0 | | BaselineSnapshotResource | 0 | 0 | 0 | 0 | 0 | 0 | ❌ | 0 | | WorkspaceResource | 0 | 0 | 0 | 0 | 0 | 0 | ❌ | 0 | | InventoryCoverage | 1–2 | 0 | 0 | 0 | 0 | 1–2 | ❌ | 0 | | BackupSetPolicyPickerTable | 3 | 0 | 0 | 1 | 0 | 5* | ❌ | 2 | | EntraGroupCachePickerTable | 2 | 0 | 0 | 0 | 0 | 2 | ❌ | 0 | | All 6 RelationManagers | 0 | 0 | 0 | 0 | 0 | 0 | ❌ | 0 | | All 7 System pages | 0 | 0 | 0 | 0 | 0 | 0 | ❌ | 0 | | All 2 Widgets | 0 | 0 | 0 | 0 | 0 | 0 | ❌ | 0 | | SettingsCatalogSettingsTable | 0 | 0 | 0 | 0 | 0 | 0 | ❌ | 0 | *BackupSetPolicyPickerTable has `SelectFilter::make('synced_within')` (custom query acting as date filter) + `SelectFilter::make('has_versions')` in addition to the standard ones. --- ## Appendix B: Quick Reference — Which Files to Touch ### P0 (Persistence only — 5 files + 1 test): - `app/Filament/Resources/InventoryItemResource.php` - `app/Filament/Resources/PolicyVersionResource.php` - `app/Filament/Resources/RestoreRunResource.php` - `app/Filament/Resources/AlertDeliveryResource.php` - `app/Filament/Resources/EntraGroupResource.php` - `tests/Feature/Guards/FilamentTableStandardsGuardTest.php` ### P1 (New filters — 5 files): - `app/Filament/Resources/RestoreRunResource.php` - `app/Filament/Resources/PolicyVersionResource.php` - `app/Filament/Resources/FindingResource.php` - `app/Filament/Resources/AlertDeliveryResource.php` - `app/Filament/Resources/InventoryItemResource.php` ### P2 (Presets + consistency — 4 files + 1 new): - `app/Support/Filament/FilterPresets.php` (new) - `app/Filament/Resources/FindingResource.php` (enum migration) - `app/Filament/Resources/AlertDeliveryResource.php` (enum migration) - `app/Filament/Resources/OperationRunResource.php` (labels) - `app/Filament/Resources/BaselineProfileResource.php` (status filter)