diff --git a/app/Filament/Resources/PolicyResource.php b/app/Filament/Resources/PolicyResource.php index bd1166c..871cfa5 100644 --- a/app/Filament/Resources/PolicyResource.php +++ b/app/Filament/Resources/PolicyResource.php @@ -59,18 +59,10 @@ public static function infolist(Schema $schema): Schema ->label('') ->view('filament.infolists.entries.normalized-settings') ->state(function (Policy $record) { - $snapshot = static::latestSnapshot($record); + $normalized = static::normalizedPolicyState($record); + $split = static::splitGeneralBlock($normalized); - $normalized = app(PolicyNormalizer::class)->normalize( - $snapshot, - $record->policy_type, - $record->platform - ); - - $normalized['context'] = 'policy'; - $normalized['record_id'] = (string) $record->getKey(); - - return $normalized; + return $split['normalized']; }) ->visible(fn (Policy $record) => $record->policy_type === 'settingsCatalogPolicy' && $record->versions()->exists() @@ -80,15 +72,10 @@ public static function infolist(Schema $schema): Schema ->label('') ->view('filament.infolists.entries.policy-settings-standard') ->state(function (Policy $record) { - $snapshot = static::latestSnapshot($record); + $normalized = static::normalizedPolicyState($record); + $split = static::splitGeneralBlock($normalized); - $normalizer = app(PolicyNormalizer::class); - - return $normalizer->normalize( - $snapshot, - $record->policy_type, - $record->platform - ); + return $split['normalized']; }) ->visible(fn (Policy $record) => $record->policy_type !== 'settingsCatalogPolicy' && $record->versions()->exists() @@ -100,6 +87,19 @@ public static function infolist(Schema $schema): Schema ->helperText('This policy has been inventoried but no configuration snapshot has been captured yet.') ->visible(fn (Policy $record) => ! $record->versions()->exists()), ]), + Tab::make('General') + ->schema([ + ViewEntry::make('policy_general') + ->label('') + ->view('filament.infolists.entries.policy-general') + ->state(function (Policy $record) { + $normalized = static::normalizedPolicyState($record); + $split = static::splitGeneralBlock($normalized); + + return $split['general']; + }), + ]) + ->visible(fn (Policy $record) => $record->versions()->exists()), Tab::make('JSON') ->schema([ ViewEntry::make('snapshot_json') @@ -331,6 +331,68 @@ private static function latestSnapshot(Policy $record): array return []; } + /** + * @return array + */ + private static function normalizedPolicyState(Policy $record): array + { + static $cache = []; + + $cacheKey = (string) $record->getKey(); + + if (isset($cache[$cacheKey])) { + return $cache[$cacheKey]; + } + + $snapshot = static::latestSnapshot($record); + + $normalized = app(PolicyNormalizer::class)->normalize( + $snapshot, + $record->policy_type, + $record->platform + ); + + $normalized['context'] = 'policy'; + $normalized['record_id'] = (string) $record->getKey(); + + $cache[$cacheKey] = $normalized; + + return $normalized; + } + + /** + * @param array{settings?: array>} $normalized + * @return array{normalized: array, general: ?array} + */ + private static function splitGeneralBlock(array $normalized): array + { + $general = null; + $filtered = []; + + foreach ($normalized['settings'] ?? [] as $block) { + if (! is_array($block)) { + continue; + } + + $title = $block['title'] ?? null; + + if (is_string($title) && strtolower($title) === 'general') { + $general = $block; + + continue; + } + + $filtered[] = $block; + } + + $normalized['settings'] = $filtered; + + return [ + 'normalized' => $normalized, + 'general' => $general, + ]; + } + /** * @return array{label:?string,category:?string,restore:?string,risk:?string}|array|array */ diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 8ce4eb0..7c5299e 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -23,7 +23,7 @@ class AdminPanelProvider extends PanelProvider { public function panel(Panel $panel): Panel { - return $panel + $panel = $panel ->default() ->id('admin') ->path('admin') @@ -55,5 +55,11 @@ public function panel(Panel $panel): Panel ->authMiddleware([ Authenticate::class, ]); + + if (! app()->runningUnitTests()) { + $panel->viteTheme('resources/css/filament/admin/theme.css'); + } + + return $panel; } } diff --git a/resources/css/filament/admin/theme.css b/resources/css/filament/admin/theme.css new file mode 100644 index 0000000..c9cc300 --- /dev/null +++ b/resources/css/filament/admin/theme.css @@ -0,0 +1,5 @@ +@import '../../../../vendor/filament/filament/resources/css/theme.css'; + +@source '../../../../app/Filament/**/*'; +@source '../../../../resources/views/filament/**/*.blade.php'; +@source '../../../../resources/views/livewire/**/*.blade.php'; diff --git a/resources/views/filament/infolists/entries/policy-general.blade.php b/resources/views/filament/infolists/entries/policy-general.blade.php new file mode 100644 index 0000000..6d46634 --- /dev/null +++ b/resources/views/filament/infolists/entries/policy-general.blade.php @@ -0,0 +1,138 @@ +@php + $general = $getState(); + $entries = is_array($general) ? ($general['entries'] ?? []) : []; + $cards = []; + + foreach ($entries as $entry) { + if (! is_array($entry)) { + continue; + } + + $key = $entry['key'] ?? null; + $value = $entry['value'] ?? null; + $decoded = null; + + if (is_string($value)) { + $trimmed = trim($value); + + if ($trimmed !== '' && (str_starts_with($trimmed, '{') || str_starts_with($trimmed, '['))) { + $decodedValue = json_decode($trimmed, true); + + if (json_last_error() === JSON_ERROR_NONE) { + $decoded = $decodedValue; + $value = $decodedValue; + } + } + } + + $isEmpty = $value === null + || $value === '' + || $value === '-' + || (is_array($value) && $value === []); + + if ($isEmpty) { + continue; + } + + $label = is_string($key) && $key !== '' ? $key : 'Field'; + + $cards[] = [ + 'key' => $label, + 'key_lower' => strtolower($label), + 'value' => $value, + 'decoded' => $decoded, + ]; + } + + $toneMap = [ + 'name' => ['icon' => 'heroicon-o-tag', 'ring' => 'ring-amber-200/70 dark:ring-amber-800/60', 'tone' => 'amber'], + 'platform' => ['icon' => 'heroicon-o-computer-desktop', 'ring' => 'ring-sky-200/70 dark:ring-sky-800/60', 'tone' => 'sky'], + 'settings' => ['icon' => 'heroicon-o-adjustments-horizontal', 'ring' => 'ring-emerald-200/70 dark:ring-emerald-800/60', 'tone' => 'emerald'], + 'template' => ['icon' => 'heroicon-o-rectangle-stack', 'ring' => 'ring-rose-200/70 dark:ring-rose-800/60', 'tone' => 'rose'], + 'technology' => ['icon' => 'heroicon-o-cpu-chip', 'ring' => 'ring-teal-200/70 dark:ring-teal-800/60', 'tone' => 'teal'], + 'default' => ['icon' => 'heroicon-o-document-text', 'ring' => 'ring-gray-200/70 dark:ring-gray-700/60', 'tone' => 'slate'], + ]; + + $toneClasses = [ + 'amber' => 'bg-amber-100/80 text-amber-700 dark:bg-amber-900/40 dark:text-amber-200', + 'sky' => 'bg-sky-100/80 text-sky-700 dark:bg-sky-900/40 dark:text-sky-200', + 'emerald' => 'bg-emerald-100/80 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-200', + 'rose' => 'bg-rose-100/80 text-rose-700 dark:bg-rose-900/40 dark:text-rose-200', + 'teal' => 'bg-teal-100/80 text-teal-700 dark:bg-teal-900/40 dark:text-teal-200', + 'slate' => 'bg-slate-100/80 text-slate-700 dark:bg-slate-900/40 dark:text-slate-200', + ]; +@endphp + +@if (empty($cards)) +

No general metadata available.

+@else +
+ @foreach ($cards as $entry) + @php + $keyLower = $entry['key_lower'] ?? ''; + $value = $entry['value'] ?? null; + $isPlatform = str_contains($keyLower, 'platform'); + $toneKey = match (true) { + str_contains($keyLower, 'name') => 'name', + str_contains($keyLower, 'platform') => 'platform', + str_contains($keyLower, 'setting') => 'settings', + str_contains($keyLower, 'template') => 'template', + str_contains($keyLower, 'technology') => 'technology', + default => 'default', + }; + $tone = $toneMap[$toneKey] ?? $toneMap['default']; + $toneClass = $toneClasses[$tone['tone'] ?? 'slate'] ?? $toneClasses['slate']; + + $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); + $isNumericValue = is_numeric($value); + @endphp + +
+
+
+ +
+
+
+ {{ $entry['key'] ?? '-' }} +
+
+ @if ($isListValue) +
+ @foreach ($value as $item) + + {{ $item }} + + @endforeach +
+ @elseif ($isJsonValue) +
{{ json_encode($value, JSON_PRETTY_PRINT) }}
+ @elseif ($isBooleanValue || $isBooleanString) + @php + $boolValue = $isBooleanValue + ? $value + : in_array(strtolower($value), ['true', 'enabled'], true); + $boolLabel = $boolValue ? 'Enabled' : 'Disabled'; + @endphp + + {{ $boolLabel }} + + @elseif ($isNumericValue) +
+ {{ number_format((float) $value) }} +
+ @else +
+ {{ is_string($value) ? $value : json_encode($value, JSON_PRETTY_PRINT) }} +
+ @endif +
+
+
+
+ @endforeach +
+@endif diff --git a/tests/Feature/Filament/PolicyViewSettingsCatalogReadableTest.php b/tests/Feature/Filament/PolicyViewSettingsCatalogReadableTest.php index a487dff..1e14ed1 100644 --- a/tests/Feature/Filament/PolicyViewSettingsCatalogReadableTest.php +++ b/tests/Feature/Filament/PolicyViewSettingsCatalogReadableTest.php @@ -78,6 +78,9 @@ $response->assertOk(); $response->assertSee('Settings'); // Settings tab should appear for Settings Catalog + $response->assertSee('General'); + $response->assertSee('JSON'); + $response->assertSee('bg-gradient-to-br'); }); it('shows display names instead of definition IDs', function () { @@ -361,7 +364,7 @@ // Value formatting verified by manual UI inspection })->skip('Requires manual UI verification - value formatting is visual'); -// T036: Test search/filter functionality +// T036: Test search/filter functionality it('search filters settings in real-time', function () { $tenant = Tenant::create([ 'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'), diff --git a/vite.config.js b/vite.config.js index f35b4e7..d9a581c 100644 --- a/vite.config.js +++ b/vite.config.js @@ -5,7 +5,11 @@ import tailwindcss from '@tailwindcss/vite'; export default defineConfig({ plugins: [ laravel({ - input: ['resources/css/app.css', 'resources/js/app.js'], + input: [ + 'resources/css/app.css', + 'resources/css/filament/admin/theme.css', + 'resources/js/app.js', + ], refresh: true, }), tailwindcss(),