feat: style policy general tab

This commit is contained in:
Ahmed Darrazi 2025-12-21 22:43:58 +01:00
parent 2b10e086ea
commit a10c4914c4
6 changed files with 240 additions and 22 deletions

View File

@ -59,18 +59,10 @@ public static function infolist(Schema $schema): Schema
->label('') ->label('')
->view('filament.infolists.entries.normalized-settings') ->view('filament.infolists.entries.normalized-settings')
->state(function (Policy $record) { ->state(function (Policy $record) {
$snapshot = static::latestSnapshot($record); $normalized = static::normalizedPolicyState($record);
$split = static::splitGeneralBlock($normalized);
$normalized = app(PolicyNormalizer::class)->normalize( return $split['normalized'];
$snapshot,
$record->policy_type,
$record->platform
);
$normalized['context'] = 'policy';
$normalized['record_id'] = (string) $record->getKey();
return $normalized;
}) })
->visible(fn (Policy $record) => $record->policy_type === 'settingsCatalogPolicy' && ->visible(fn (Policy $record) => $record->policy_type === 'settingsCatalogPolicy' &&
$record->versions()->exists() $record->versions()->exists()
@ -80,15 +72,10 @@ public static function infolist(Schema $schema): Schema
->label('') ->label('')
->view('filament.infolists.entries.policy-settings-standard') ->view('filament.infolists.entries.policy-settings-standard')
->state(function (Policy $record) { ->state(function (Policy $record) {
$snapshot = static::latestSnapshot($record); $normalized = static::normalizedPolicyState($record);
$split = static::splitGeneralBlock($normalized);
$normalizer = app(PolicyNormalizer::class); return $split['normalized'];
return $normalizer->normalize(
$snapshot,
$record->policy_type,
$record->platform
);
}) })
->visible(fn (Policy $record) => $record->policy_type !== 'settingsCatalogPolicy' && ->visible(fn (Policy $record) => $record->policy_type !== 'settingsCatalogPolicy' &&
$record->versions()->exists() $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.') ->helperText('This policy has been inventoried but no configuration snapshot has been captured yet.')
->visible(fn (Policy $record) => ! $record->versions()->exists()), ->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') Tab::make('JSON')
->schema([ ->schema([
ViewEntry::make('snapshot_json') ViewEntry::make('snapshot_json')
@ -331,6 +331,68 @@ private static function latestSnapshot(Policy $record): array
return []; return [];
} }
/**
* @return array<string, mixed>
*/
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<int, array<string, mixed>>} $normalized
* @return array{normalized: array<string, mixed>, general: ?array<string, mixed>}
*/
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<string,string>|array<string,mixed> * @return array{label:?string,category:?string,restore:?string,risk:?string}|array<string,string>|array<string,mixed>
*/ */

View File

@ -23,7 +23,7 @@ class AdminPanelProvider extends PanelProvider
{ {
public function panel(Panel $panel): Panel public function panel(Panel $panel): Panel
{ {
return $panel $panel = $panel
->default() ->default()
->id('admin') ->id('admin')
->path('admin') ->path('admin')
@ -55,5 +55,11 @@ public function panel(Panel $panel): Panel
->authMiddleware([ ->authMiddleware([
Authenticate::class, Authenticate::class,
]); ]);
if (! app()->runningUnitTests()) {
$panel->viteTheme('resources/css/filament/admin/theme.css');
}
return $panel;
} }
} }

View File

@ -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';

View File

@ -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))
<p class="text-sm text-gray-600 dark:text-gray-400">No general metadata available.</p>
@else
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
@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
<div class="group relative overflow-hidden rounded-xl border border-gray-200/70 bg-gradient-to-br from-white via-amber-50/40 to-amber-100/60 p-4 shadow-sm transition duration-200 hover:-translate-y-0.5 hover:border-amber-200/70 hover:shadow-md dark:border-gray-700/60 dark:from-gray-950 dark:via-gray-900 dark:to-amber-950/30 dark:hover:border-amber-700/60">
<div class="flex items-start gap-3">
<div class="flex h-10 w-10 items-center justify-center rounded-lg ring-1 {{ $tone['ring'] ?? '' }} {{ $toneClass }}">
<x-filament::icon icon="{{ $tone['icon'] ?? 'heroicon-o-document-text' }}" class="h-5 w-5" />
</div>
<div class="min-w-0 flex-1">
<dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
{{ $entry['key'] ?? '-' }}
</dt>
<dd class="mt-2">
@if ($isListValue)
<div class="flex flex-wrap gap-2">
@foreach ($value as $item)
<x-filament::badge :color="$isPlatform ? 'info' : 'gray'" size="sm">
{{ $item }}
</x-filament::badge>
@endforeach
</div>
@elseif ($isJsonValue)
<pre class="whitespace-pre-wrap rounded-lg border border-gray-200 bg-gray-50 p-2 text-xs font-mono text-gray-700 dark:border-gray-700 dark:bg-gray-900/60 dark:text-gray-200">{{ json_encode($value, JSON_PRETTY_PRINT) }}</pre>
@elseif ($isBooleanValue || $isBooleanString)
@php
$boolValue = $isBooleanValue
? $value
: in_array(strtolower($value), ['true', 'enabled'], true);
$boolLabel = $boolValue ? 'Enabled' : 'Disabled';
@endphp
<x-filament::badge :color="$boolValue ? 'success' : 'gray'" size="sm">
{{ $boolLabel }}
</x-filament::badge>
@elseif ($isNumericValue)
<div class="text-lg font-semibold text-gray-900 dark:text-white tabular-nums">
{{ number_format((float) $value) }}
</div>
@else
<div class="text-sm text-gray-900 dark:text-white whitespace-pre-wrap break-words">
{{ is_string($value) ? $value : json_encode($value, JSON_PRETTY_PRINT) }}
</div>
@endif
</dd>
</div>
</div>
</div>
@endforeach
</div>
@endif

View File

@ -78,6 +78,9 @@
$response->assertOk(); $response->assertOk();
$response->assertSee('Settings'); // Settings tab should appear for Settings Catalog $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 () { it('shows display names instead of definition IDs', function () {
@ -361,7 +364,7 @@
// Value formatting verified by manual UI inspection // Value formatting verified by manual UI inspection
})->skip('Requires manual UI verification - value formatting is visual'); })->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 () { it('search filters settings in real-time', function () {
$tenant = Tenant::create([ $tenant = Tenant::create([
'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'), 'tenant_id' => env('INTUNE_TENANT_ID', 'local-tenant'),

View File

@ -5,7 +5,11 @@ import tailwindcss from '@tailwindcss/vite';
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
laravel({ 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, refresh: true,
}), }),
tailwindcss(), tailwindcss(),