From 1bc6600fcc244d36cc019732b7284ec028218a5e Mon Sep 17 00:00:00 2001 From: ahmido Date: Fri, 23 Jan 2026 23:05:55 +0000 Subject: [PATCH] feat: tag badge catalog (060) (#72) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: completes Feature 060 by adding the suite-wide TagBadge catalog (spec/domain/renderer) plus migration notes/tests/docs/specs/plan/checklist. standardizes all inert “tag-like” badges (policy type/category/platform, tenant environment, backup schedule frequency, etc.) to use the new catalog so only neutral colors are emitted. fixes remaining Feature 059 regressions (inventory run/restore badges, Inventory Coverage tables, Boolean-enabled streak) and adds the BooleanEnabled badge mappings/guards/tests plus new QA tasks/checklist. Testing: BooleanEnabledBadgesTest.php PolicyGeneralViewTest.php PolicySettingsStandardViewTest.php SettingsCatalogPolicyNormalizedDisplayTest.php PolicyViewSettingsCatalogReadableTest.php (partial/visual checks skipped) TagBadgeCatalogTest.php TagBadgePaletteInvariantTest.php NoForbiddenTagBadgeColorsTest.php NoAdHocStatusBadgesTest.php Manual QA per quickstart.md confirmed. Next steps: Co-authored-by: Ahmed Darrazi Reviewed-on: https://git.cloudarix.de/ahmido/TenantAtlas/pulls/72 --- .../Resources/BackupScheduleResource.php | 23 +- .../BackupItemsRelationManager.php | 10 +- app/Filament/Resources/EntraGroupResource.php | 18 +- .../Resources/InventoryItemResource.php | 73 +++- app/Filament/Resources/PolicyResource.php | 19 +- .../VersionsRelationManager.php | 8 +- .../Resources/PolicyVersionResource.php | 22 +- app/Filament/Resources/TenantResource.php | 10 +- app/Livewire/BackupSetPolicyPickerTable.php | 10 +- app/Support/Badges/TagBadgeCatalog.php | 324 ++++++++++++++++++ app/Support/Badges/TagBadgeDomain.php | 14 + app/Support/Badges/TagBadgeRenderer.php | 43 +++ app/Support/Badges/TagBadgeSpec.php | 48 +++ .../entries/policy-general.blade.php | 31 +- .../policy-settings-standard.blade.php | 77 +++-- .../settings-catalog-grouped.blade.php | 49 ++- .../pages/inventory-coverage.blade.php | 96 +++++- .../checklists/requirements.md | 35 ++ .../contracts/guardrails.md | 29 ++ .../contracts/tag-badge-semantics.md | 56 +++ specs/060-tag-badge-catalog/data-model.md | 55 +++ specs/060-tag-badge-catalog/discovery.md | 75 ++++ specs/060-tag-badge-catalog/plan.md | 145 ++++++++ specs/060-tag-badge-catalog/quickstart.md | 26 ++ specs/060-tag-badge-catalog/research.md | 77 +++++ specs/060-tag-badge-catalog/spec.md | 118 +++++++ specs/060-tag-badge-catalog/tasks.md | 174 ++++++++++ .../Guards/NoForbiddenTagBadgeColorsTest.php | 135 ++++++++ .../Unit/Badges/BooleanEnabledBadgesTest.php | 27 ++ tests/Unit/Badges/TagBadgeCatalogTest.php | 90 +++++ .../Badges/TagBadgePaletteInvariantTest.php | 41 +++ 31 files changed, 1841 insertions(+), 117 deletions(-) create mode 100644 app/Support/Badges/TagBadgeCatalog.php create mode 100644 app/Support/Badges/TagBadgeDomain.php create mode 100644 app/Support/Badges/TagBadgeRenderer.php create mode 100644 app/Support/Badges/TagBadgeSpec.php create mode 100644 specs/060-tag-badge-catalog/checklists/requirements.md create mode 100644 specs/060-tag-badge-catalog/contracts/guardrails.md create mode 100644 specs/060-tag-badge-catalog/contracts/tag-badge-semantics.md create mode 100644 specs/060-tag-badge-catalog/data-model.md create mode 100644 specs/060-tag-badge-catalog/discovery.md create mode 100644 specs/060-tag-badge-catalog/plan.md create mode 100644 specs/060-tag-badge-catalog/quickstart.md create mode 100644 specs/060-tag-badge-catalog/research.md create mode 100644 specs/060-tag-badge-catalog/spec.md create mode 100644 specs/060-tag-badge-catalog/tasks.md create mode 100644 tests/Feature/Guards/NoForbiddenTagBadgeColorsTest.php create mode 100644 tests/Unit/Badges/BooleanEnabledBadgesTest.php create mode 100644 tests/Unit/Badges/TagBadgeCatalogTest.php create mode 100644 tests/Unit/Badges/TagBadgePaletteInvariantTest.php diff --git a/app/Filament/Resources/BackupScheduleResource.php b/app/Filament/Resources/BackupScheduleResource.php index d2d6d9a..53a8721 100644 --- a/app/Filament/Resources/BackupScheduleResource.php +++ b/app/Filament/Resources/BackupScheduleResource.php @@ -17,6 +17,8 @@ use App\Services\OperationRunService; use App\Support\Badges\BadgeDomain; use App\Support\Badges\BadgeRenderer; +use App\Support\Badges\TagBadgeDomain; +use App\Support\Badges\TagBadgeRenderer; use App\Support\OperationRunLinks; use App\Support\OpsUx\OperationUxPresenter; use App\Support\OpsUx\OpsUxBrowserEvents; @@ -39,7 +41,6 @@ use Filament\Resources\Resource; use Filament\Schemas\Components\Utilities\Get; use Filament\Schemas\Schema; -use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Contracts\HasTable; use Filament\Tables\Filters\SelectFilter; @@ -175,9 +176,13 @@ public static function table(Table $table): Table return $table ->defaultSort('next_run_at', 'asc') ->columns([ - IconColumn::make('is_enabled') + TextColumn::make('is_enabled') ->label('Enabled') - ->boolean() + ->badge() + ->formatStateUsing(BadgeRenderer::label(BadgeDomain::BooleanEnabled)) + ->color(BadgeRenderer::color(BadgeDomain::BooleanEnabled)) + ->icon(BadgeRenderer::icon(BadgeDomain::BooleanEnabled)) + ->iconColor(BadgeRenderer::iconColor(BadgeDomain::BooleanEnabled)) ->alignCenter(), TextColumn::make('name') @@ -187,16 +192,8 @@ public static function table(Table $table): Table TextColumn::make('frequency') ->label('Frequency') ->badge() - ->formatStateUsing(fn (?string $state): string => match ($state) { - 'daily' => 'Daily', - 'weekly' => 'Weekly', - default => (string) $state, - }) - ->color(fn (?string $state): string => match ($state) { - 'daily' => 'success', - 'weekly' => 'warning', - default => 'gray', - }), + ->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::BackupScheduleFrequency)) + ->color(TagBadgeRenderer::color(TagBadgeDomain::BackupScheduleFrequency)), TextColumn::make('time_of_day') ->label('Time') diff --git a/app/Filament/Resources/BackupSetResource/RelationManagers/BackupItemsRelationManager.php b/app/Filament/Resources/BackupSetResource/RelationManagers/BackupItemsRelationManager.php index cd7ad76..9f0ab28 100644 --- a/app/Filament/Resources/BackupSetResource/RelationManagers/BackupItemsRelationManager.php +++ b/app/Filament/Resources/BackupSetResource/RelationManagers/BackupItemsRelationManager.php @@ -10,6 +10,8 @@ use App\Services\OperationRunService; use App\Support\Badges\BadgeDomain; use App\Support\Badges\BadgeRenderer; +use App\Support\Badges\TagBadgeDomain; +use App\Support\Badges\TagBadgeRenderer; use App\Support\OperationRunLinks; use App\Support\OpsUx\OperationUxPresenter; use App\Support\OpsUx\OpsUxBrowserEvents; @@ -53,7 +55,8 @@ public function table(Table $table): Table Tables\Columns\TextColumn::make('policy_type') ->label('Type') ->badge() - ->formatStateUsing(fn (?string $state) => static::typeMeta($state)['label'] ?? $state), + ->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::PolicyType)) + ->color(TagBadgeRenderer::color(TagBadgeDomain::PolicyType)), Tables\Columns\TextColumn::make('restore_mode') ->label('Restore') ->badge() @@ -73,7 +76,10 @@ public function table(Table $table): Table Tables\Columns\TextColumn::make('policy_identifier') ->label('Policy ID') ->copyable(), - Tables\Columns\TextColumn::make('platform')->badge(), + Tables\Columns\TextColumn::make('platform') + ->badge() + ->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::Platform)) + ->color(TagBadgeRenderer::color(TagBadgeDomain::Platform)), Tables\Columns\TextColumn::make('assignments') ->label('Assignments') ->badge() diff --git a/app/Filament/Resources/EntraGroupResource.php b/app/Filament/Resources/EntraGroupResource.php index 2479125..2b0b441 100644 --- a/app/Filament/Resources/EntraGroupResource.php +++ b/app/Filament/Resources/EntraGroupResource.php @@ -5,6 +5,8 @@ use App\Filament\Resources\EntraGroupResource\Pages; use App\Models\EntraGroup; use App\Models\Tenant; +use App\Support\Badges\BadgeDomain; +use App\Support\Badges\BadgeRenderer; use BackedEnum; use Filament\Actions; use Filament\Infolists\Components\TextEntry; @@ -46,8 +48,20 @@ public static function infolist(Schema $schema): Schema TextEntry::make('type') ->badge() ->state(fn (EntraGroup $record): string => static::groupTypeLabel(static::groupType($record))), - TextEntry::make('security_enabled')->label('Security')->badge(), - TextEntry::make('mail_enabled')->label('Mail')->badge(), + TextEntry::make('security_enabled') + ->label('Security') + ->badge() + ->formatStateUsing(BadgeRenderer::label(BadgeDomain::BooleanEnabled)) + ->color(BadgeRenderer::color(BadgeDomain::BooleanEnabled)) + ->icon(BadgeRenderer::icon(BadgeDomain::BooleanEnabled)) + ->iconColor(BadgeRenderer::iconColor(BadgeDomain::BooleanEnabled)), + TextEntry::make('mail_enabled') + ->label('Mail') + ->badge() + ->formatStateUsing(BadgeRenderer::label(BadgeDomain::BooleanEnabled)) + ->color(BadgeRenderer::color(BadgeDomain::BooleanEnabled)) + ->icon(BadgeRenderer::icon(BadgeDomain::BooleanEnabled)) + ->iconColor(BadgeRenderer::iconColor(BadgeDomain::BooleanEnabled)), TextEntry::make('last_seen_at')->label('Last seen')->dateTime()->placeholder('—'), ]) ->columns(2) diff --git a/app/Filament/Resources/InventoryItemResource.php b/app/Filament/Resources/InventoryItemResource.php index 3c342dd..cb1fbe9 100644 --- a/app/Filament/Resources/InventoryItemResource.php +++ b/app/Filament/Resources/InventoryItemResource.php @@ -8,6 +8,10 @@ use App\Models\Tenant; use App\Services\Inventory\DependencyQueryService; use App\Services\Inventory\DependencyTargets\DependencyTargetResolver; +use App\Support\Badges\BadgeDomain; +use App\Support\Badges\BadgeRenderer; +use App\Support\Badges\TagBadgeDomain; +use App\Support\Badges\TagBadgeRenderer; use App\Support\Enums\RelationshipType; use App\Support\Inventory\InventoryPolicyTypeMeta; use BackedEnum; @@ -49,12 +53,18 @@ public static function infolist(Schema $schema): Schema TextEntry::make('policy_type') ->label('Type') ->badge() - ->state(fn (InventoryItem $record): string => static::typeMeta($record->policy_type)['label'] ?? (string) $record->policy_type), + ->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::PolicyType)) + ->color(TagBadgeRenderer::color(TagBadgeDomain::PolicyType)), TextEntry::make('category') ->badge() - ->state(fn (InventoryItem $record): string => $record->category - ?: (static::typeMeta($record->policy_type)['category'] ?? 'Unknown')), - TextEntry::make('platform')->badge(), + ->state(fn (InventoryItem $record): ?string => $record->category + ?: (static::typeMeta($record->policy_type)['category'] ?? null)) + ->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::PolicyCategory)) + ->color(TagBadgeRenderer::color(TagBadgeDomain::PolicyCategory)), + TextEntry::make('platform') + ->badge() + ->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::Platform)) + ->color(TagBadgeRenderer::color(TagBadgeDomain::Platform)), TextEntry::make('external_id')->label('External ID'), TextEntry::make('last_seen_at')->label('Last seen')->dateTime(), TextEntry::make('last_seen_run_id') @@ -70,11 +80,19 @@ public static function infolist(Schema $schema): Schema TextEntry::make('support_restore') ->label('Restore') ->badge() - ->state(fn (InventoryItem $record): string => static::typeMeta($record->policy_type)['restore'] ?? 'enabled'), + ->state(fn (InventoryItem $record): string => static::typeMeta($record->policy_type)['restore'] ?? 'enabled') + ->formatStateUsing(BadgeRenderer::label(BadgeDomain::PolicyRestoreMode)) + ->color(BadgeRenderer::color(BadgeDomain::PolicyRestoreMode)) + ->icon(BadgeRenderer::icon(BadgeDomain::PolicyRestoreMode)) + ->iconColor(BadgeRenderer::iconColor(BadgeDomain::PolicyRestoreMode)), TextEntry::make('support_risk') ->label('Risk') ->badge() - ->state(fn (InventoryItem $record): string => static::typeMeta($record->policy_type)['risk'] ?? 'normal'), + ->state(fn (InventoryItem $record): string => static::typeMeta($record->policy_type)['risk'] ?? 'normal') + ->formatStateUsing(BadgeRenderer::label(BadgeDomain::PolicyRisk)) + ->color(BadgeRenderer::color(BadgeDomain::PolicyRisk)) + ->icon(BadgeRenderer::icon(BadgeDomain::PolicyRisk)) + ->iconColor(BadgeRenderer::iconColor(BadgeDomain::PolicyRisk)), ]) ->columns(2) ->columnSpanFull(), @@ -144,17 +162,52 @@ public static function table(Table $table): Table Tables\Columns\TextColumn::make('policy_type') ->label('Type') ->badge() - ->formatStateUsing(fn (?string $state): string => static::typeMeta($state)['label'] ?? (string) $state), + ->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::PolicyType)) + ->color(TagBadgeRenderer::color(TagBadgeDomain::PolicyType)), Tables\Columns\TextColumn::make('category') - ->badge(), + ->badge() + ->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::PolicyCategory)) + ->color(TagBadgeRenderer::color(TagBadgeDomain::PolicyCategory)), Tables\Columns\TextColumn::make('platform') - ->badge(), + ->badge() + ->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::Platform)) + ->color(TagBadgeRenderer::color(TagBadgeDomain::Platform)), Tables\Columns\TextColumn::make('last_seen_at') ->label('Last seen') ->since(), Tables\Columns\TextColumn::make('lastSeenRun.status') ->label('Run') - ->badge(), + ->badge() + ->formatStateUsing(function (?string $state): string { + if (! filled($state)) { + return '—'; + } + + return BadgeRenderer::spec(BadgeDomain::InventorySyncRunStatus, $state)->label; + }) + ->color(function (?string $state): string { + if (! filled($state)) { + return 'gray'; + } + + return BadgeRenderer::spec(BadgeDomain::InventorySyncRunStatus, $state)->color; + }) + ->icon(function (?string $state): ?string { + if (! filled($state)) { + return null; + } + + return BadgeRenderer::spec(BadgeDomain::InventorySyncRunStatus, $state)->icon; + }) + ->iconColor(function (?string $state): ?string { + if (! filled($state)) { + return 'gray'; + } + + $spec = BadgeRenderer::spec(BadgeDomain::InventorySyncRunStatus, $state); + + return $spec->iconColor ?? $spec->color; + }), ]) ->filters([ Tables\Filters\SelectFilter::make('policy_type') diff --git a/app/Filament/Resources/PolicyResource.php b/app/Filament/Resources/PolicyResource.php index 1d082cf..8d34514 100644 --- a/app/Filament/Resources/PolicyResource.php +++ b/app/Filament/Resources/PolicyResource.php @@ -16,6 +16,8 @@ use App\Services\Operations\BulkSelectionIdentity; use App\Support\Badges\BadgeDomain; use App\Support\Badges\BadgeRenderer; +use App\Support\Badges\TagBadgeDomain; +use App\Support\Badges\TagBadgeRenderer; use App\Support\OperationRunLinks; use App\Support\OpsUx\OperationUxPresenter; use App\Support\OpsUx\OpsUxBrowserEvents; @@ -241,17 +243,28 @@ public static function table(Table $table): Table Tables\Columns\TextColumn::make('policy_type') ->label('Type') ->badge() - ->formatStateUsing(fn (?string $state, Policy $record) => static::typeMeta($record->policy_type)['label'] ?? $state), + ->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::PolicyType)) + ->color(TagBadgeRenderer::color(TagBadgeDomain::PolicyType)) + ->icon(TagBadgeRenderer::icon(TagBadgeDomain::PolicyType)) + ->iconColor(TagBadgeRenderer::iconColor(TagBadgeDomain::PolicyType)), Tables\Columns\TextColumn::make('category') ->label('Category') ->badge() - ->state(fn (Policy $record) => static::typeMeta($record->policy_type)['category'] ?? 'Unknown'), + ->state(fn (Policy $record) => static::typeMeta($record->policy_type)['category'] ?? null) + ->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::PolicyCategory)) + ->color(TagBadgeRenderer::color(TagBadgeDomain::PolicyCategory)), Tables\Columns\TextColumn::make('restore_mode') ->label('Restore') ->badge() - ->state(fn (Policy $record) => static::typeMeta($record->policy_type)['restore'] ?? 'enabled'), + ->state(fn (Policy $record) => static::typeMeta($record->policy_type)['restore'] ?? 'enabled') + ->formatStateUsing(BadgeRenderer::label(BadgeDomain::PolicyRestoreMode)) + ->color(BadgeRenderer::color(BadgeDomain::PolicyRestoreMode)) + ->icon(BadgeRenderer::icon(BadgeDomain::PolicyRestoreMode)) + ->iconColor(BadgeRenderer::iconColor(BadgeDomain::PolicyRestoreMode)), Tables\Columns\TextColumn::make('platform') ->badge() + ->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::Platform)) + ->color(TagBadgeRenderer::color(TagBadgeDomain::Platform)) ->sortable(), Tables\Columns\TextColumn::make('settings_status') ->label('Settings') diff --git a/app/Filament/Resources/PolicyResource/RelationManagers/VersionsRelationManager.php b/app/Filament/Resources/PolicyResource/RelationManagers/VersionsRelationManager.php index 5a340ab..7885ded 100644 --- a/app/Filament/Resources/PolicyResource/RelationManagers/VersionsRelationManager.php +++ b/app/Filament/Resources/PolicyResource/RelationManagers/VersionsRelationManager.php @@ -6,6 +6,8 @@ use App\Models\PolicyVersion; use App\Models\Tenant; use App\Services\Intune\RestoreService; +use App\Support\Badges\TagBadgeDomain; +use App\Support\Badges\TagBadgeRenderer; use Filament\Actions; use Filament\Forms; use Filament\Notifications\Notification; @@ -24,7 +26,11 @@ public function table(Table $table): Table Tables\Columns\TextColumn::make('version_number')->sortable(), Tables\Columns\TextColumn::make('captured_at')->dateTime()->sortable(), Tables\Columns\TextColumn::make('created_by')->label('Actor'), - Tables\Columns\TextColumn::make('policy_type')->badge()->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('policy_type') + ->badge() + ->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::PolicyType)) + ->color(TagBadgeRenderer::color(TagBadgeDomain::PolicyType)) + ->toggleable(isToggledHiddenByDefault: true), ]) ->defaultSort('version_number', 'desc') ->filters([]) diff --git a/app/Filament/Resources/PolicyVersionResource.php b/app/Filament/Resources/PolicyVersionResource.php index 19f0319..4e5a338 100644 --- a/app/Filament/Resources/PolicyVersionResource.php +++ b/app/Filament/Resources/PolicyVersionResource.php @@ -16,6 +16,8 @@ use App\Services\Intune\VersionDiff; use App\Services\OperationRunService; use App\Services\Operations\BulkSelectionIdentity; +use App\Support\Badges\TagBadgeDomain; +use App\Support\Badges\TagBadgeRenderer; use App\Support\OperationRunLinks; use App\Support\OpsUx\OperationUxPresenter; use BackedEnum; @@ -52,8 +54,14 @@ public static function infolist(Schema $schema): Schema ->schema([ Infolists\Components\TextEntry::make('policy.display_name')->label('Policy'), Infolists\Components\TextEntry::make('version_number')->label('Version'), - Infolists\Components\TextEntry::make('policy_type'), - Infolists\Components\TextEntry::make('platform'), + Infolists\Components\TextEntry::make('policy_type') + ->badge() + ->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::PolicyType)) + ->color(TagBadgeRenderer::color(TagBadgeDomain::PolicyType)), + Infolists\Components\TextEntry::make('platform') + ->badge() + ->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::Platform)) + ->color(TagBadgeRenderer::color(TagBadgeDomain::Platform)), Infolists\Components\TextEntry::make('created_by')->label('Actor'), Infolists\Components\TextEntry::make('captured_at')->dateTime(), Tabs::make() @@ -177,8 +185,14 @@ public static function table(Table $table): Table ->columns([ Tables\Columns\TextColumn::make('policy.display_name')->label('Policy')->sortable()->searchable(), Tables\Columns\TextColumn::make('version_number')->sortable(), - Tables\Columns\TextColumn::make('policy_type')->badge(), - Tables\Columns\TextColumn::make('platform')->badge(), + Tables\Columns\TextColumn::make('policy_type') + ->badge() + ->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::PolicyType)) + ->color(TagBadgeRenderer::color(TagBadgeDomain::PolicyType)), + Tables\Columns\TextColumn::make('platform') + ->badge() + ->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::Platform)) + ->color(TagBadgeRenderer::color(TagBadgeDomain::Platform)), Tables\Columns\TextColumn::make('created_by')->label('Actor'), Tables\Columns\TextColumn::make('captured_at')->dateTime()->sortable(), ]) diff --git a/app/Filament/Resources/TenantResource.php b/app/Filament/Resources/TenantResource.php index 2d5504b..9caeaef 100644 --- a/app/Filament/Resources/TenantResource.php +++ b/app/Filament/Resources/TenantResource.php @@ -19,6 +19,8 @@ use App\Services\Operations\BulkSelectionIdentity; use App\Support\Badges\BadgeDomain; use App\Support\Badges\BadgeRenderer; +use App\Support\Badges\TagBadgeDomain; +use App\Support\Badges\TagBadgeRenderer; use App\Support\OperationRunLinks; use App\Support\OpsUx\OperationUxPresenter; use App\Support\OpsUx\OpsUxBrowserEvents; @@ -128,12 +130,8 @@ public static function table(Table $table): Table ->searchable(), Tables\Columns\TextColumn::make('environment') ->badge() - ->color(fn (?string $state) => match ($state) { - 'prod' => 'danger', - 'dev' => 'warning', - 'staging' => 'info', - default => 'gray', - }) + ->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::TenantEnvironment)) + ->color(TagBadgeRenderer::color(TagBadgeDomain::TenantEnvironment)) ->sortable(), Tables\Columns\TextColumn::make('policies_count') ->label('Policies') diff --git a/app/Livewire/BackupSetPolicyPickerTable.php b/app/Livewire/BackupSetPolicyPickerTable.php index 3e601dd..db2e721 100644 --- a/app/Livewire/BackupSetPolicyPickerTable.php +++ b/app/Livewire/BackupSetPolicyPickerTable.php @@ -11,6 +11,8 @@ use App\Services\Operations\BulkSelectionIdentity; use App\Support\Badges\BadgeDomain; use App\Support\Badges\BadgeRenderer; +use App\Support\Badges\TagBadgeDomain; +use App\Support\Badges\TagBadgeRenderer; use App\Support\OperationRunLinks; use App\Support\OpsUx\OperationUxPresenter; use App\Support\OpsUx\OpsUxBrowserEvents; @@ -83,11 +85,15 @@ public function table(Table $table): Table TextColumn::make('policy_type') ->label('Type') ->badge() - ->formatStateUsing(fn (?string $state): string => (string) (static::typeMeta($state)['label'] ?? $state ?? '—')), + ->placeholder('—') + ->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::PolicyType)) + ->color(TagBadgeRenderer::color(TagBadgeDomain::PolicyType)), TextColumn::make('platform') ->label('Platform') ->badge() - ->default('—') + ->placeholder('—') + ->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::Platform)) + ->color(TagBadgeRenderer::color(TagBadgeDomain::Platform)) ->sortable(), TextColumn::make('external_id') ->label('External ID') diff --git a/app/Support/Badges/TagBadgeCatalog.php b/app/Support/Badges/TagBadgeCatalog.php new file mode 100644 index 0000000..118d0be --- /dev/null +++ b/app/Support/Badges/TagBadgeCatalog.php @@ -0,0 +1,324 @@ +> + */ + private static array $policyTypeMetaCache = []; + + /** + * @var array + */ + private static array $policyCategoryLabelCache = []; + + public static function spec(TagBadgeDomain $domain, mixed $value): TagBadgeSpec + { + return match ($domain) { + TagBadgeDomain::PolicyType => self::policyType($value), + TagBadgeDomain::PolicyCategory => self::policyCategory($value), + TagBadgeDomain::Platform => self::platform($value), + TagBadgeDomain::TenantEnvironment => self::tenantEnvironment($value), + TagBadgeDomain::Visibility => self::visibility($value), + TagBadgeDomain::SnapshotType => self::snapshotType($value), + TagBadgeDomain::BackupScheduleFrequency => self::backupScheduleFrequency($value), + }; + } + + public static function normalizeValue(mixed $value): ?string + { + if ($value === null) { + return null; + } + + if ($value instanceof BackedEnum) { + $value = $value->value; + } + + if ($value instanceof Stringable) { + $value = (string) $value; + } + + if (is_bool($value)) { + return $value ? 'true' : 'false'; + } + + if (is_int($value) || is_float($value)) { + return (string) $value; + } + + if (! is_string($value)) { + return null; + } + + $value = trim($value); + + return $value === '' ? null : $value; + } + + private static function policyType(mixed $value): TagBadgeSpec + { + $type = self::normalizeValue($value); + + if (! filled($type)) { + return new TagBadgeSpec('Other: —', self::colorForDomain(TagBadgeDomain::PolicyType)); + } + + $meta = self::policyTypeMeta($type); + $label = $meta['label'] ?? null; + + if (is_string($label) && $label !== '') { + return new TagBadgeSpec($label, self::colorForDomain(TagBadgeDomain::PolicyType)); + } + + return self::other(TagBadgeDomain::PolicyType, $type); + } + + private static function policyCategory(mixed $value): TagBadgeSpec + { + $category = self::normalizeValue($value); + + if (! filled($category)) { + return new TagBadgeSpec('Other: —', self::colorForDomain(TagBadgeDomain::PolicyCategory)); + } + + $normalized = Str::of($category) + ->trim() + ->lower() + ->replaceMatches('/\s+/', ' ') + ->toString(); + + $label = self::policyCategoryLabels()[$normalized] ?? null; + + if (is_string($label) && $label !== '') { + return new TagBadgeSpec($label, self::colorForDomain(TagBadgeDomain::PolicyCategory)); + } + + return self::other(TagBadgeDomain::PolicyCategory, $category); + } + + private static function platform(mixed $value): TagBadgeSpec + { + $platform = self::normalizeValue($value); + + if (! filled($platform)) { + return new TagBadgeSpec('Other: —', self::colorForDomain(TagBadgeDomain::Platform)); + } + + $normalized = Str::of($platform) + ->trim() + ->lower() + ->replace(['_', '-'], ' ') + ->replaceMatches('/\s+/', ' ') + ->toString(); + + $label = match ($normalized) { + 'windows' => 'Windows', + 'android' => 'Android', + 'ios' => 'iOS', + 'macos' => 'macOS', + 'all' => 'All', + 'mobile' => 'Mobile', + default => null, + }; + + if (is_string($label)) { + return new TagBadgeSpec($label, self::colorForDomain(TagBadgeDomain::Platform)); + } + + return self::other(TagBadgeDomain::Platform, $platform); + } + + private static function tenantEnvironment(mixed $value): TagBadgeSpec + { + $environment = self::normalizeValue($value); + + if (! filled($environment)) { + return new TagBadgeSpec('Other: —', self::colorForDomain(TagBadgeDomain::TenantEnvironment)); + } + + $normalized = Str::of($environment) + ->trim() + ->lower() + ->replace(['_', '-'], ' ') + ->replaceMatches('/\s+/', ' ') + ->toString(); + + $label = match ($normalized) { + 'prod', 'production' => 'Prod', + 'staging' => 'Staging', + 'dev', 'development' => 'Dev', + default => null, + }; + + if (is_string($label)) { + return new TagBadgeSpec($label, self::colorForDomain(TagBadgeDomain::TenantEnvironment)); + } + + return self::other(TagBadgeDomain::TenantEnvironment, $environment); + } + + private static function visibility(mixed $value): TagBadgeSpec + { + $visibility = self::normalizeValue($value); + + if (! filled($visibility)) { + return new TagBadgeSpec('Other: —', self::colorForDomain(TagBadgeDomain::Visibility)); + } + + $normalized = Str::of($visibility) + ->trim() + ->lower() + ->replace(['_', '-'], ' ') + ->replaceMatches('/\s+/', ' ') + ->toString(); + + $label = match ($normalized) { + 'active' => 'Active', + 'archived' => 'Archived', + default => null, + }; + + if (is_string($label)) { + return new TagBadgeSpec($label, self::colorForDomain(TagBadgeDomain::Visibility)); + } + + return self::other(TagBadgeDomain::Visibility, $visibility); + } + + private static function snapshotType(mixed $value): TagBadgeSpec + { + $type = self::normalizeValue($value); + + if (! filled($type)) { + return new TagBadgeSpec('Other: —', self::colorForDomain(TagBadgeDomain::SnapshotType)); + } + + $normalized = Str::of($type) + ->trim() + ->lower() + ->replace(['_', '-'], ' ') + ->replaceMatches('/\s+/', ' ') + ->toString(); + + $label = match ($normalized) { + 'full' => 'Full', + 'incremental' => 'Incremental', + default => null, + }; + + if (is_string($label)) { + return new TagBadgeSpec($label, self::colorForDomain(TagBadgeDomain::SnapshotType)); + } + + return self::other(TagBadgeDomain::SnapshotType, $type); + } + + private static function backupScheduleFrequency(mixed $value): TagBadgeSpec + { + $frequency = self::normalizeValue($value); + + if (! filled($frequency)) { + return new TagBadgeSpec('Other: —', self::colorForDomain(TagBadgeDomain::BackupScheduleFrequency)); + } + + $normalized = Str::of($frequency) + ->trim() + ->lower() + ->replace(['_', '-'], ' ') + ->replaceMatches('/\s+/', ' ') + ->toString(); + + $label = match ($normalized) { + 'daily' => 'Daily', + 'weekly' => 'Weekly', + default => null, + }; + + if (is_string($label)) { + return new TagBadgeSpec($label, self::colorForDomain(TagBadgeDomain::BackupScheduleFrequency)); + } + + return self::other(TagBadgeDomain::BackupScheduleFrequency, $frequency); + } + + private static function other(TagBadgeDomain $domain, string $value): TagBadgeSpec + { + $normalizedValue = Str::of($value) + ->trim() + ->replaceMatches('/\s+/', ' ') + ->toString(); + + return new TagBadgeSpec( + label: 'Other: '.$normalizedValue, + color: self::colorForDomain($domain), + ); + } + + private static function colorForDomain(TagBadgeDomain $domain): string + { + return match ($domain) { + TagBadgeDomain::PolicyType => 'primary', + TagBadgeDomain::PolicyCategory => 'gray', + TagBadgeDomain::Platform => 'info', + TagBadgeDomain::TenantEnvironment => 'gray', + TagBadgeDomain::Visibility => 'gray', + TagBadgeDomain::SnapshotType => 'primary', + TagBadgeDomain::BackupScheduleFrequency => 'info', + }; + } + + /** + * @return array + */ + private static function policyTypeMeta(string $type): array + { + if (self::$policyTypeMetaCache === []) { + self::$policyTypeMetaCache = InventoryPolicyTypeMeta::byType(); + } + + return self::$policyTypeMetaCache[$type] ?? []; + } + + /** + * @return array + */ + private static function policyCategoryLabels(): array + { + if (self::$policyCategoryLabelCache !== []) { + return self::$policyCategoryLabelCache; + } + + $labels = []; + + foreach (InventoryPolicyTypeMeta::all() as $meta) { + if (! is_array($meta)) { + continue; + } + + $category = $meta['category'] ?? null; + if (! is_string($category) || trim($category) === '') { + continue; + } + + $normalized = Str::of($category) + ->trim() + ->lower() + ->replaceMatches('/\s+/', ' ') + ->toString(); + + $labels[$normalized] ??= $category; + } + + self::$policyCategoryLabelCache = $labels; + + return self::$policyCategoryLabelCache; + } +} diff --git a/app/Support/Badges/TagBadgeDomain.php b/app/Support/Badges/TagBadgeDomain.php new file mode 100644 index 0000000..27bc625 --- /dev/null +++ b/app/Support/Badges/TagBadgeDomain.php @@ -0,0 +1,14 @@ +label; + }; + } + + public static function color(TagBadgeDomain $domain): Closure + { + return static function (mixed $state, mixed ...$args) use ($domain): string { + return TagBadgeCatalog::spec($domain, $state)->color; + }; + } + + public static function icon(TagBadgeDomain $domain): Closure + { + return static function (mixed $state, mixed ...$args) use ($domain): ?string { + return TagBadgeCatalog::spec($domain, $state)->icon; + }; + } + + public static function iconColor(TagBadgeDomain $domain): Closure + { + return static function (mixed $state, mixed ...$args) use ($domain): ?string { + $spec = TagBadgeCatalog::spec($domain, $state); + + return $spec->iconColor ?? $spec->color; + }; + } + + public static function spec(TagBadgeDomain $domain, mixed $state): TagBadgeSpec + { + return TagBadgeCatalog::spec($domain, $state); + } +} diff --git a/app/Support/Badges/TagBadgeSpec.php b/app/Support/Badges/TagBadgeSpec.php new file mode 100644 index 0000000..8ae6159 --- /dev/null +++ b/app/Support/Badges/TagBadgeSpec.php @@ -0,0 +1,48 @@ + + */ + private const ALLOWED_COLORS = [ + 'gray', + 'primary', + 'info', + ]; + + public function __construct( + public readonly string $label, + public readonly string $color, + public readonly ?string $icon = null, + public readonly ?string $iconColor = null, + ) { + if (trim($this->label) === '') { + throw new InvalidArgumentException('TagBadgeSpec label must be a non-empty string.'); + } + + if (! in_array($this->color, self::ALLOWED_COLORS, true)) { + throw new InvalidArgumentException('TagBadgeSpec color must be one of: '.implode(', ', self::ALLOWED_COLORS)); + } + + if ($this->icon !== null && trim($this->icon) === '') { + throw new InvalidArgumentException('TagBadgeSpec icon must be null or a non-empty string.'); + } + + if ($this->iconColor !== null && ! in_array($this->iconColor, self::ALLOWED_COLORS, true)) { + throw new InvalidArgumentException('TagBadgeSpec iconColor must be null or one of: '.implode(', ', self::ALLOWED_COLORS)); + } + } + + /** + * @return array + */ + public static function allowedColors(): array + { + return self::ALLOWED_COLORS; + } +} diff --git a/resources/views/filament/infolists/entries/policy-general.blade.php b/resources/views/filament/infolists/entries/policy-general.blade.php index c22bc0a..2003863 100644 --- a/resources/views/filament/infolists/entries/policy-general.blade.php +++ b/resources/views/filament/infolists/entries/policy-general.blade.php @@ -1,4 +1,8 @@ @php + use App\Support\Badges\BadgeCatalog; + use App\Support\Badges\BadgeDomain; + use App\Support\Badges\TagBadgeCatalog; + use App\Support\Badges\TagBadgeDomain; use Carbon\CarbonImmutable; use Illuminate\Support\Str; @@ -113,7 +117,7 @@ $isJsonValue = is_array($value) && ! (array_is_list($value) && array_reduce($value, fn ($carry, $item) => $carry && is_scalar($item), true)); $isListValue = is_array($value) && array_is_list($value) && array_reduce($value, fn ($carry, $item) => $carry && is_scalar($item), true); $isBooleanValue = is_bool($value); - $isBooleanString = is_string($value) && in_array(strtolower($value), ['true', 'false', 'enabled', 'disabled'], true); + $isBooleanString = is_string($value) && in_array(strtolower($value), ['true', 'false', 'enabled', 'disabled', 'yes', 'no', '1', '0'], true); $isNumericValue = is_numeric($value); $badgeItems = null; @@ -173,22 +177,29 @@ @elseif (is_array($badgeItems) && $badgeItems !== [])
@foreach ($badgeItems as $item) - - {{ $item }} - + @if ($isPlatform) + @php + $spec = TagBadgeCatalog::spec(TagBadgeDomain::Platform, $item); + @endphp + + + {{ $spec->label }} + + @else + + {{ $item }} + + @endif @endforeach
@elseif ($isJsonValue)
{{ json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) }}
@elseif ($isBooleanValue || $isBooleanString) @php - $boolValue = $isBooleanValue - ? $value - : in_array(strtolower($value), ['true', 'enabled'], true); - $boolLabel = $boolValue ? 'Enabled' : 'Disabled'; + $boolSpec = BadgeCatalog::spec(BadgeDomain::BooleanEnabled, $value); @endphp - - {{ $boolLabel }} + + {{ $boolSpec->label }} @elseif ($isNumericValue)
diff --git a/resources/views/filament/infolists/entries/policy-settings-standard.blade.php b/resources/views/filament/infolists/entries/policy-settings-standard.blade.php index 32dac8e..9d15c88 100644 --- a/resources/views/filament/infolists/entries/policy-settings-standard.blade.php +++ b/resources/views/filament/infolists/entries/policy-settings-standard.blade.php @@ -1,4 +1,7 @@ @php + use App\Support\Badges\BadgeCatalog; + use App\Support\Badges\BadgeDomain; + use App\Support\Badges\BadgeSpec; use Illuminate\Support\Str; // Extract state from Filament ViewEntry @@ -60,22 +63,10 @@ return true; }; - $asEnabledDisabledBadgeValue = function (mixed $value): ?bool { - if (is_bool($value)) { - return $value; - } + $asEnabledDisabledBadgeSpec = function (mixed $value): ?BadgeSpec { + $spec = BadgeCatalog::spec(BadgeDomain::BooleanEnabled, $value); - if (! is_string($value)) { - return null; - } - - $normalized = strtolower(trim($value)); - - return match ($normalized) { - 'enabled', 'true', 'yes', '1' => true, - 'disabled', 'false', 'no', '0' => false, - default => null, - }; + return $spec->label === 'Unknown' ? null : $spec; }; @endphp @@ -117,12 +108,12 @@
@php - $badgeValue = $asEnabledDisabledBadgeValue($row['value'] ?? null); + $badgeSpec = $asEnabledDisabledBadgeSpec($row['value'] ?? null); @endphp - @if(! is_null($badgeValue)) - - {{ $badgeValue ? 'Enabled' : 'Disabled' }} + @if($badgeSpec) + + {{ $badgeSpec->label }} @elseif(is_numeric($row['value'])) {{ $row['value'] }} @@ -171,12 +162,12 @@
@php - $badgeValue = $asEnabledDisabledBadgeValue($row['value'] ?? null); + $badgeSpec = $asEnabledDisabledBadgeSpec($row['value'] ?? null); @endphp - @if(! is_null($badgeValue)) - - {{ $badgeValue ? 'Enabled' : 'Disabled' }} + @if($badgeSpec) + + {{ $badgeSpec->label }} @elseif(is_numeric($row['value'])) @@ -185,9 +176,19 @@ @elseif($shouldRenderBadges($row['value'] ?? null))
@foreach(($row['value'] ?? []) as $item) - - {{ is_bool($item) ? ($item ? 'Enabled' : 'Disabled') : (string) $item }} - + @php + $itemSpec = $asEnabledDisabledBadgeSpec($item); + @endphp + + @if($itemSpec) + + {{ $itemSpec->label }} + + @else + + {{ is_null($item) ? '—' : (string) $item }} + + @endif @endforeach
@else @@ -225,7 +226,7 @@ $isScriptContent = in_array($entry['key'] ?? null, ['scriptContent', 'detectionScriptContent', 'remediationScriptContent'], true) && (bool) config('tenantpilot.display.show_script_content', false); - $badgeValue = $asEnabledDisabledBadgeValue($rawValue); + $badgeSpec = $asEnabledDisabledBadgeSpec($rawValue); @endphp @if($isScriptContent) @@ -305,14 +306,24 @@ @elseif($shouldRenderBadges($rawValue))
@foreach(($rawValue ?? []) as $item) - - {{ is_bool($item) ? ($item ? 'Enabled' : 'Disabled') : (string) $item }} - + @php + $itemSpec = $asEnabledDisabledBadgeSpec($item); + @endphp + + @if($itemSpec) + + {{ $itemSpec->label }} + + @else + + {{ is_null($item) ? '—' : (string) $item }} + + @endif @endforeach
- @elseif(! is_null($badgeValue)) - - {{ $badgeValue ? 'Enabled' : 'Disabled' }} + @elseif($badgeSpec) + + {{ $badgeSpec->label }} @else diff --git a/resources/views/filament/infolists/entries/settings-catalog-grouped.blade.php b/resources/views/filament/infolists/entries/settings-catalog-grouped.blade.php index 9414337..dab54c3 100644 --- a/resources/views/filament/infolists/entries/settings-catalog-grouped.blade.php +++ b/resources/views/filament/infolists/entries/settings-catalog-grouped.blade.php @@ -1,4 +1,6 @@ @php + use App\Support\Badges\BadgeCatalog; + use App\Support\Badges\BadgeDomain; use Illuminate\Support\Str; // Extract groups from Filament ViewEntry state using $getState() @@ -71,25 +73,34 @@
{{ $setting['label'] }}
-
-
- @if(is_bool($setting['value_raw'])) - - {{ $setting['value_display'] }} - - @elseif(is_int($setting['value_raw'])) - - {{ $setting['value_display'] }} - - @elseif(str_contains(strtolower($setting['value_display']), 'enabled') || str_contains(strtolower($setting['value_display']), 'disabled')) - - {{ $setting['value_display'] }} - - @else - - {{ $setting['value_display'] }} - - @endif +
+
+ @php + $rawValue = $setting['value_raw'] ?? null; + $displayValue = $setting['value_display'] ?? null; + + $booleanSpec = BadgeCatalog::spec(BadgeDomain::BooleanEnabled, $rawValue); + + if ($booleanSpec->label === 'Unknown') { + $booleanSpec = BadgeCatalog::spec(BadgeDomain::BooleanEnabled, $displayValue); + } + + $hasBooleanBadge = $booleanSpec->label !== 'Unknown'; + @endphp + + @if($hasBooleanBadge) + + {{ $booleanSpec->label }} + + @elseif(is_int($rawValue)) + + {{ $displayValue }} + + @else + + {{ $displayValue }} + + @endif @if(strlen($setting['value_display'] ?? '') > 200)