## Summary - add canonical UI standards under `docs/product/standards/` - add a comprehensive filter audit as source material for future filter standardization work - extend the constitution with incremental UI standards enforcement guidance ## Included - `docs/product/standards/README.md` - `docs/product/standards/filament-table-ux.md` - `docs/product/standards/filament-filter-ux.md` - `docs/product/standards/filament-actions-ux.md` - `docs/product/standards/list-surface-review-checklist.md` - `docs/audits/filter-audit-comprehensive.md` - `.specify/memory/constitution.md` ## Notes - this is documentation and governance work only; no runtime code paths changed - no tests were run because the change is docs-only - the new standards structure separates permanent principles, living standards, rollout audits, and review checklists ## Review Focus - confirm the standards location under `docs/product/standards/` - confirm the constitution principle belongs at the constitutional level - confirm the filter audit should live under `docs/audits/` as reference material Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #153
28 KiB
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_typeoptions come from config — could be->searchable()for large lists - ⚠️
platformoptions 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_keyandrun_idsare advanced/developer filters — should be in an "Advanced" section - ⚠️ No date range filter (created_at, due_at exist but are not filterable)
- ⚠️
statushas 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
- ⚠️
typeuses 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
- ⚠️
providerfilter 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 ✅:
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
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
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
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)
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
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
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
- Persistence: All Tier 1 and Tier 2 surfaces MUST have
->persistFiltersInSession(),->persistSearchInSession(),->persistSortInSession(). - Soft-delete: Every resource with
SoftDeletesMUST have the standardTrashedFilterwith Active/All/Archived labels. - Status enums: All status filters MUST source options from the model's enum — no manual string arrays.
- Date range: Surfaces with time-series data (created_at, captured_at, started_at) in Tier 1–2 SHOULD have a date range filter.
- Defaults: Surfaces that commonly have "inactive" records (ignored, archived, completed) SHOULD default to showing only "active" records.
7.3 Filter Catalog (Extractable Presets)
// 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
FilamentTableStandardsGuardTestenforces: 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
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
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:
FilterPresetshelper class with extractable patterns- Persistence trio added to all Tier 1–2 surfaces
- Date range filters added to 5 time-series surfaces
- Status filters migrated to enum-based options
- Guard test expanded with filter-specific assertions
- TrashedFilter standardized across all soft-deletable resources
Dependencies:
- Builds on completed
125-table-ux-standardizationspec - 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.phpapp/Filament/Resources/PolicyVersionResource.phpapp/Filament/Resources/RestoreRunResource.phpapp/Filament/Resources/AlertDeliveryResource.phpapp/Filament/Resources/EntraGroupResource.phptests/Feature/Guards/FilamentTableStandardsGuardTest.php
P1 (New filters — 5 files):
app/Filament/Resources/RestoreRunResource.phpapp/Filament/Resources/PolicyVersionResource.phpapp/Filament/Resources/FindingResource.phpapp/Filament/Resources/AlertDeliveryResource.phpapp/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)