fix: enterprise UX overhaul for baseline governance
- Fix display_name → name column on Tenant model (3 files) - Rename 'Soll vs Ist' to 'Baseline Compare' (English consistency) - Redesign landing page: stats overview grid, critical drift banner, severity breakdown section, proper empty states with icons - Upgrade dashboard widget: severity badges, inline critical alert, last compared timestamp, compliance status indicator - Move Findings + DriftLanding from 'Drift' to 'Governance' nav group
This commit is contained in:
parent
9085403b9b
commit
a98323d2f4
@ -1,22 +1,19 @@
|
||||
<!--
|
||||
Sync Impact Report
|
||||
|
||||
- Version change: 1.8.1 → 1.8.2
|
||||
- Version change: 1.8.2 → 1.9.0
|
||||
- Modified principles:
|
||||
- RBAC Context — Planes, Roles, and Auditability (clarified admin vs tenant-context vs workspace-context)
|
||||
- Tenant Isolation is Non-negotiable (added scope + ownership rules)
|
||||
- RBAC-UX-007 — Global search must be tenant-safe (added workspace-context rules)
|
||||
- Filament UI — Action Surface Contract (NON-NEGOTIABLE) (added required spec scope fields)
|
||||
- Filament UI — Action Surface Contract (NON-NEGOTIABLE) (tightened UX requirements; added layout/view/empty-state rules)
|
||||
- Added sections:
|
||||
- Scope & Ownership Clarification (SCOPE-001)
|
||||
- Spec Scope Fields (SCOPE-002)
|
||||
- Filament UI — Layout & Information Architecture Standards (UX-001)
|
||||
- Removed sections: None
|
||||
- Templates requiring updates:
|
||||
- ✅ .specify/templates/plan-template.md
|
||||
- ✅ .specify/templates/spec-template.md
|
||||
- ✅ .specify/templates/tasks-template.md
|
||||
- N/A: .specify/templates/commands/ (directory not present in this repo)
|
||||
- Follow-up TODOs: None
|
||||
- Follow-up TODOs:
|
||||
- Add CI regression guards for “no naked forms” + “view must use infolist” (heuristic scan) in test suite.
|
||||
-->
|
||||
|
||||
# TenantPilot Constitution
|
||||
@ -179,7 +176,7 @@ ### Filament UI — Action Surface Contract (NON-NEGOTIABLE)
|
||||
- Accepted forms: clickable rows via `recordUrl()` (preferred), a dedicated row “View” action, or a primary linked column.
|
||||
- Rule: Do NOT render a lone “View” row action button. If View is the only row action, prefer clickable rows.
|
||||
- View/Detail MUST define Header Actions (Edit + “More” group when applicable).
|
||||
- View/Detail SHOULD be sectioned (e.g., Infolist Sections / Cards); avoid long ungrouped field lists.
|
||||
- View/Detail MUST be sectioned (e.g., Infolist Sections / Cards); avoid long ungrouped field lists.
|
||||
- Create/Edit MUST provide consistent Save/Cancel UX.
|
||||
|
||||
Grouping & safety
|
||||
@ -199,6 +196,38 @@ ### Filament UI — Action Surface Contract (NON-NEGOTIABLE)
|
||||
- A change is not “Done” unless the Action Surface Contract is met OR an explicit exemption exists with documented reason.
|
||||
- CI MUST run an automated Action Surface Contract check (test suite and/or command) that fails when required surfaces are missing.
|
||||
|
||||
### Filament UI — Layout & Information Architecture Standards (UX-001)
|
||||
|
||||
Goal: Demo-level, enterprise-grade admin UX. These rules are NON-NEGOTIABLE for new or modified Filament screens.
|
||||
|
||||
Page layout
|
||||
- Create/Edit MUST default to a Main/Aside layout using a 3-column grid with `Main=columnSpan(2)` and `Aside=columnSpan(1)`.
|
||||
- All fields MUST be inside Sections/Cards. No “naked” inputs at the root schema level.
|
||||
- Main contains domain definition/content. Aside contains status/meta (status, version label, owner, scope selectors, timestamps).
|
||||
- Related data (assignments, relations, evidence, runs, findings, etc.) MUST render as separate Sections below the main/aside grid (or as tabs/sub-navigation), never mixed as unstructured long lists.
|
||||
|
||||
View pages
|
||||
- View/Detail MUST be a read-only experience using Infolists (or equivalent), not disabled edit forms.
|
||||
- Status-like values MUST render as badges/chips using the centralized badge semantics (BADGE-001).
|
||||
- Long text MUST render as readable prose (not textarea styling).
|
||||
|
||||
Empty states
|
||||
- Empty lists/tables MUST show: a specific title, one-sentence explanation, and exactly ONE primary CTA in the empty state.
|
||||
- When non-empty, the primary CTA MUST move to the table header (top-right) and MUST NOT be duplicated in the empty state.
|
||||
|
||||
Actions & flows
|
||||
- Pages SHOULD expose at most 1 primary header action and 1 secondary action; all other actions MUST be grouped (ActionGroup / BulkActionGroup).
|
||||
- Multi-step or high-risk flows MUST use a Wizard (e.g., capture/compare/restore with preview + confirmation).
|
||||
- Destructive actions MUST remain non-primary and require confirmation (RBAC-UX-005).
|
||||
|
||||
Table work-surface defaults
|
||||
- Tables SHOULD provide search (when the dataset can grow), a meaningful default sort, and filters for core dimensions (status/severity/type/tenant/time-range).
|
||||
- Tables MUST render key statuses as badges/chips (BADGE-001) and avoid ad-hoc status mappings.
|
||||
|
||||
Enforcement
|
||||
- New resources/pages SHOULD use shared layout builders (e.g., `MainAsideForm`, `MainAsideInfolist`, `StandardTableDefaults`) to keep screens consistent.
|
||||
- A change is not “Done” unless UX-001 is satisfied OR an explicit exemption exists with a documented rationale in the spec/PR.
|
||||
|
||||
Spec Scope Fields (SCOPE-002)
|
||||
|
||||
- Every feature spec MUST declare:
|
||||
@ -245,4 +274,4 @@ ### Versioning Policy (SemVer)
|
||||
- **MINOR**: new principle/section or materially expanded guidance.
|
||||
- **MAJOR**: removing/redefining principles in a backward-incompatible way.
|
||||
|
||||
**Version**: 1.8.2 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-02-14
|
||||
**Version**: 1.9.0 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-02-19
|
||||
|
||||
@ -44,8 +44,7 @@ ## Constitution Check
|
||||
- Automation: queued/scheduled ops use locks + idempotency; handle 429/503 with backoff+jitter
|
||||
- Data minimization: Inventory stores metadata + whitelisted meta; logs contain no secrets/tokens
|
||||
- Badge semantics (BADGE-001): status-like badges use `BadgeCatalog` / `BadgeRenderer`; no ad-hoc mappings; new values include tests
|
||||
- Filament UI Action Surface Contract: for any new/modified Filament Resource/RelationManager/Page, define Header/Row/Bulk/Empty-State actions, ensure every List/Table has a record inspection affordance (prefer `recordUrl()` clickable rows; do not render a lone View row action), keep max 2 visible row actions with the rest in “More”, group bulk actions, require confirmations for destructive actions (typed confirmation for large/bulk where applicable), write audit logs for mutations, enforce RBAC via central helpers (non-member 404, member missing capability 403), and ensure CI blocks merges if the contract is violated or not explicitly exempted
|
||||
|
||||
- Filament UI Action Surface Contract: for any new/modified Filament Resource/RelationManager/Page, define Header/Row/Bulk/Empty-State actions, ensure every List/Table has a record inspection affordance (prefer `recordUrl()` clickable rows; do not render a lone View row action), keep max 2 visible row actions with the rest in “More”, group bulk actions, require confirmations for destructive actions (typed confirmation for large/bulk where applicable), write audit logs for mutations, enforce RBAC via central helpers (non-member 404, member missing capability 403), and ensure CI blocks merges if the contract is violated or not explicitly exempted- Filament UI UX-001 (Layout & IA): Create/Edit uses Main/Aside (3-col grid, Main=columnSpan(2), Aside=columnSpan(1)); all fields inside Sections/Cards (no naked inputs); View uses Infolists (not disabled edit forms); status badges use BADGE-001; empty states have specific title + explanation + 1 CTA; max 1 primary + 1 secondary header action; tables provide search/sort/filters for core dimensions; shared layout builders preferred for consistency
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
@ -115,7 +115,11 @@ ## Requirements *(mandatory)*
|
||||
**Constitution alignment (Filament Action Surfaces):** If this feature adds or modifies any Filament Resource / RelationManager / Page,
|
||||
the spec MUST include a “UI Action Matrix” (see below) and explicitly state whether the Action Surface Contract is satisfied.
|
||||
If the contract is not satisfied, the spec MUST include an explicit exemption with rationale.
|
||||
|
||||
**Constitution alignment (UX-001 — Layout & Information Architecture):** If this feature adds or modifies any Filament screen,
|
||||
the spec MUST describe compliance with UX-001: Create/Edit uses Main/Aside layout (3-col grid), all fields inside Sections/Cards
|
||||
(no naked inputs), View pages use Infolists (not disabled edit forms), status badges use BADGE-001, empty states have a specific
|
||||
title + explanation + exactly 1 CTA, and tables provide search/sort/filters for core dimensions.
|
||||
If UX-001 is not fully satisfied, the spec MUST include an explicit exemption with documented rationale.
|
||||
<!--
|
||||
ACTION REQUIRED: The content in this section represents placeholders.
|
||||
Fill them out with the right functional requirements.
|
||||
|
||||
@ -34,6 +34,14 @@ # Tasks: [FEATURE NAME]
|
||||
- adding confirmations for destructive actions (and typed confirmation where required by scale),
|
||||
- adding `AuditLog` entries for relevant mutations,
|
||||
- adding/updated tests that enforce the contract and block merge on violations, OR documenting an explicit exemption with rationale.
|
||||
**Filament UI UX-001 (Layout & IA)**: If this feature adds/modifies any Filament screen, tasks MUST include:
|
||||
- ensuring Create/Edit pages use Main/Aside layout (3-col grid, Main=columnSpan(2), Aside=columnSpan(1)),
|
||||
- ensuring all form fields are inside Sections/Cards (no naked inputs at root schema level),
|
||||
- ensuring View pages use Infolists (not disabled edit forms); status badges use BADGE-001,
|
||||
- ensuring empty states show a specific title + explanation + exactly 1 CTA; non-empty tables move CTA to header,
|
||||
- capping header actions to max 1 primary + 1 secondary (rest grouped),
|
||||
- using shared layout builders (e.g., `MainAsideForm`, `MainAsideInfolist`, `StandardTableDefaults`) where available,
|
||||
- OR documenting an explicit exemption with rationale if UX-001 is not fully satisfied.
|
||||
**Badges**: If this feature changes status-like badge semantics, tasks MUST use `BadgeCatalog` / `BadgeRenderer` (BADGE-001),
|
||||
avoid ad-hoc mappings in Filament, and include mapping tests for any new/changed values.
|
||||
|
||||
|
||||
@ -14,7 +14,6 @@
|
||||
use App\Services\Baselines\BaselineCompareService;
|
||||
use App\Support\Auth\Capabilities;
|
||||
use App\Support\OperationRunLinks;
|
||||
use App\Support\OpsUx\OperationUxPresenter;
|
||||
use BackedEnum;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
@ -27,9 +26,11 @@ class BaselineCompareLanding extends Page
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'Governance';
|
||||
|
||||
protected static ?string $navigationLabel = 'Soll vs Ist';
|
||||
protected static ?string $navigationLabel = 'Baseline Compare';
|
||||
|
||||
protected static ?int $navigationSort = 30;
|
||||
protected static ?int $navigationSort = 10;
|
||||
|
||||
protected static ?string $title = 'Baseline Compare';
|
||||
|
||||
protected string $view = 'filament.pages.baseline-compare-landing';
|
||||
|
||||
@ -50,6 +51,8 @@ class BaselineCompareLanding extends Page
|
||||
/** @var array<string, int>|null */
|
||||
public ?array $severityCounts = null;
|
||||
|
||||
public ?string $lastComparedAt = null;
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
$user = auth()->user();
|
||||
@ -125,6 +128,10 @@ public function mount(): void
|
||||
return;
|
||||
}
|
||||
|
||||
if ($latestRun instanceof OperationRun && $latestRun->finished_at !== null) {
|
||||
$this->lastComparedAt = $latestRun->finished_at->diffForHumans();
|
||||
}
|
||||
|
||||
$scopeKey = 'baseline_profile:'.$profile->getKey();
|
||||
|
||||
$findingsQuery = Finding::query()
|
||||
|
||||
@ -28,7 +28,7 @@ class DriftLanding extends Page
|
||||
{
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-arrows-right-left';
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'Drift';
|
||||
protected static string|UnitEnum|null $navigationGroup = 'Governance';
|
||||
|
||||
protected static ?string $navigationLabel = 'Drift';
|
||||
|
||||
|
||||
@ -109,8 +109,8 @@ private function getWorkspaceTenantOptions(): array
|
||||
|
||||
return Tenant::query()
|
||||
->where('workspace_id', $workspaceId)
|
||||
->orderBy('display_name')
|
||||
->pluck('display_name', 'id')
|
||||
->orderBy('name')
|
||||
->pluck('name', 'id')
|
||||
->all();
|
||||
}
|
||||
|
||||
|
||||
@ -43,7 +43,7 @@ public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('tenant.display_name')
|
||||
Tables\Columns\TextColumn::make('tenant.name')
|
||||
->label('Tenant')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('assignedByUser.name')
|
||||
@ -178,13 +178,13 @@ private function getAvailableTenantOptions(): array
|
||||
|
||||
$query = Tenant::query()
|
||||
->where('workspace_id', $profile->workspace_id)
|
||||
->orderBy('display_name');
|
||||
->orderBy('name');
|
||||
|
||||
if (! empty($assignedTenantIds)) {
|
||||
$query->whereNotIn('id', $assignedTenantIds);
|
||||
}
|
||||
|
||||
return $query->pluck('display_name', 'id')->all();
|
||||
return $query->pluck('name', 'id')->all();
|
||||
}
|
||||
|
||||
private function auditAssignment(
|
||||
|
||||
@ -46,7 +46,7 @@ class FindingResource extends Resource
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-exclamation-triangle';
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'Drift';
|
||||
protected static string|UnitEnum|null $navigationGroup = 'Governance';
|
||||
|
||||
protected static ?string $navigationLabel = 'Findings';
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
namespace App\Filament\Widgets\Dashboard;
|
||||
|
||||
use App\Models\BaselineCompareRun;
|
||||
use App\Models\BaselineTenantAssignment;
|
||||
use App\Models\Finding;
|
||||
use App\Models\Tenant;
|
||||
@ -23,14 +24,19 @@ protected function getViewData(): array
|
||||
{
|
||||
$tenant = Filament::getTenant();
|
||||
|
||||
if (! $tenant instanceof Tenant) {
|
||||
return [
|
||||
$empty = [
|
||||
'hasAssignment' => false,
|
||||
'profileName' => null,
|
||||
'findingsCount' => 0,
|
||||
'highCount' => 0,
|
||||
'mediumCount' => 0,
|
||||
'lowCount' => 0,
|
||||
'lastComparedAt' => null,
|
||||
'landingUrl' => null,
|
||||
];
|
||||
|
||||
if (! $tenant instanceof Tenant) {
|
||||
return $empty;
|
||||
}
|
||||
|
||||
$assignment = BaselineTenantAssignment::query()
|
||||
@ -39,13 +45,7 @@ protected function getViewData(): array
|
||||
->first();
|
||||
|
||||
if (! $assignment instanceof BaselineTenantAssignment || $assignment->baselineProfile === null) {
|
||||
return [
|
||||
'hasAssignment' => false,
|
||||
'profileName' => null,
|
||||
'findingsCount' => 0,
|
||||
'highCount' => 0,
|
||||
'landingUrl' => null,
|
||||
];
|
||||
return $empty;
|
||||
}
|
||||
|
||||
$profile = $assignment->baselineProfile;
|
||||
@ -62,12 +62,28 @@ protected function getViewData(): array
|
||||
$highCount = (int) (clone $findingsQuery)
|
||||
->where('severity', Finding::SEVERITY_HIGH)
|
||||
->count();
|
||||
$mediumCount = (int) (clone $findingsQuery)
|
||||
->where('severity', Finding::SEVERITY_MEDIUM)
|
||||
->count();
|
||||
$lowCount = (int) (clone $findingsQuery)
|
||||
->where('severity', Finding::SEVERITY_LOW)
|
||||
->count();
|
||||
|
||||
$latestRun = BaselineCompareRun::query()
|
||||
->where('tenant_id', $tenant->getKey())
|
||||
->where('baseline_profile_id', $profile->getKey())
|
||||
->whereNotNull('finished_at')
|
||||
->latest('finished_at')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'hasAssignment' => true,
|
||||
'profileName' => (string) $profile->name,
|
||||
'findingsCount' => $findingsCount,
|
||||
'highCount' => $highCount,
|
||||
'mediumCount' => $mediumCount,
|
||||
'lowCount' => $lowCount,
|
||||
'lastComparedAt' => $latestRun?->finished_at?->diffForHumans(),
|
||||
'landingUrl' => \App\Filament\Pages\BaselineCompareLanding::getUrl(tenant: $tenant),
|
||||
];
|
||||
}
|
||||
|
||||
@ -1,93 +1,188 @@
|
||||
<x-filament::page>
|
||||
{{-- Row 1: Stats Overview --}}
|
||||
@if (in_array($state, ['ready', 'idle', 'comparing']))
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
{{-- Stat: Assigned Baseline --}}
|
||||
<x-filament::section>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
Compare the current tenant state against the assigned baseline profile to detect drift.
|
||||
</div>
|
||||
|
||||
@if (filled($profileName))
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<span class="font-medium text-gray-950 dark:text-white">Baseline Profile:</span>
|
||||
<span class="text-gray-700 dark:text-gray-200">{{ $profileName }}</span>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">Assigned Baseline</div>
|
||||
<div class="text-lg font-semibold text-gray-950 dark:text-white">{{ $profileName ?? '—' }}</div>
|
||||
@if ($snapshotId)
|
||||
<x-filament::badge color="success" size="sm">
|
||||
<x-filament::badge color="success" size="sm" class="w-fit">
|
||||
Snapshot #{{ $snapshotId }}
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</x-filament::section>
|
||||
|
||||
@if ($state === 'no_tenant')
|
||||
<x-filament::badge color="gray">No tenant selected</x-filament::badge>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">{{ $message }}</div>
|
||||
@elseif ($state === 'no_assignment')
|
||||
<x-filament::badge color="gray">No baseline assigned</x-filament::badge>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">{{ $message }}</div>
|
||||
@elseif ($state === 'no_snapshot')
|
||||
<x-filament::badge color="warning">No snapshot available</x-filament::badge>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">{{ $message }}</div>
|
||||
@elseif ($state === 'comparing')
|
||||
<div class="flex items-center gap-2">
|
||||
<x-filament::badge color="info">Comparing…</x-filament::badge>
|
||||
<x-filament::loading-indicator class="h-4 w-4" />
|
||||
{{-- Stat: Total Findings --}}
|
||||
<x-filament::section>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Findings</div>
|
||||
<div class="text-3xl font-bold {{ ($findingsCount ?? 0) > 0 ? 'text-danger-600 dark:text-danger-400' : 'text-success-600 dark:text-success-400' }}">
|
||||
{{ $findingsCount ?? 0 }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">{{ $message }}</div>
|
||||
@if ($state === 'comparing')
|
||||
<div class="flex items-center gap-1 text-sm text-info-600 dark:text-info-400">
|
||||
<x-filament::loading-indicator class="h-3 w-3" />
|
||||
Comparing…
|
||||
</div>
|
||||
@elseif (($findingsCount ?? 0) === 0 && $state === 'ready')
|
||||
<span class="text-sm text-success-600 dark:text-success-400">All clear</span>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
{{-- Stat: Last Compared --}}
|
||||
<x-filament::section>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">Last Compared</div>
|
||||
<div class="text-lg font-semibold text-gray-950 dark:text-white">
|
||||
{{ $lastComparedAt ?? 'Never' }}
|
||||
</div>
|
||||
@if ($this->getRunUrl())
|
||||
<x-filament::link :href="$this->getRunUrl()" size="sm">
|
||||
View operation run
|
||||
View run
|
||||
</x-filament::link>
|
||||
@endif
|
||||
@elseif ($state === 'idle')
|
||||
<x-filament::badge color="gray">Ready to compare</x-filament::badge>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">{{ $message }}</div>
|
||||
@elseif ($state === 'ready')
|
||||
@if ($findingsCount !== null && $findingsCount > 0)
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<x-filament::badge color="danger" size="sm">
|
||||
{{ $findingsCount }} {{ Str::plural('finding', $findingsCount) }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Critical drift banner --}}
|
||||
@if ($state === 'ready' && ($severityCounts['high'] ?? 0) > 0)
|
||||
<div class="rounded-lg border border-danger-300 bg-danger-50 p-4 dark:border-danger-700 dark:bg-danger-950/50">
|
||||
<div class="flex items-start gap-3">
|
||||
<x-heroicon-s-exclamation-triangle class="h-6 w-6 shrink-0 text-danger-600 dark:text-danger-400" />
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-base font-semibold text-danger-800 dark:text-danger-200">
|
||||
Critical Drift Detected
|
||||
</div>
|
||||
<div class="text-sm text-danger-700 dark:text-danger-300">
|
||||
The current tenant state deviates from baseline <strong>{{ $profileName }}</strong>.
|
||||
{{ $severityCounts['high'] }} high-severity {{ Str::plural('finding', $severityCounts['high']) }} require immediate attention.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- State: No tenant / no assignment / no snapshot --}}
|
||||
@if (in_array($state, ['no_tenant', 'no_assignment', 'no_snapshot']))
|
||||
<x-filament::section>
|
||||
<div class="flex flex-col items-center justify-center gap-3 py-8 text-center">
|
||||
@if ($state === 'no_tenant')
|
||||
<x-heroicon-o-building-office class="h-12 w-12 text-gray-400 dark:text-gray-500" />
|
||||
<div class="text-lg font-semibold text-gray-950 dark:text-white">No Tenant Selected</div>
|
||||
@elseif ($state === 'no_assignment')
|
||||
<x-heroicon-o-link-slash class="h-12 w-12 text-gray-400 dark:text-gray-500" />
|
||||
<div class="text-lg font-semibold text-gray-950 dark:text-white">No Baseline Assigned</div>
|
||||
@elseif ($state === 'no_snapshot')
|
||||
<x-heroicon-o-camera class="h-12 w-12 text-warning-400 dark:text-warning-500" />
|
||||
<div class="text-lg font-semibold text-gray-950 dark:text-white">No Snapshot Available</div>
|
||||
@endif
|
||||
<div class="max-w-md text-sm text-gray-500 dark:text-gray-400">{{ $message }}</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
{{-- Severity breakdown + actions --}}
|
||||
@if ($state === 'ready' && ($findingsCount ?? 0) > 0)
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">
|
||||
{{ $findingsCount }} {{ Str::plural('Finding', $findingsCount) }}
|
||||
</x-slot>
|
||||
<x-slot name="description">
|
||||
The tenant configuration drifted from the baseline profile.
|
||||
</x-slot>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
@if (($severityCounts['high'] ?? 0) > 0)
|
||||
<x-filament::badge color="danger" size="sm">
|
||||
{{ $severityCounts['high'] }} high
|
||||
<x-filament::badge color="danger">
|
||||
{{ $severityCounts['high'] }} High
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
|
||||
@if (($severityCounts['medium'] ?? 0) > 0)
|
||||
<x-filament::badge color="warning" size="sm">
|
||||
{{ $severityCounts['medium'] }} medium
|
||||
<x-filament::badge color="warning">
|
||||
{{ $severityCounts['medium'] }} Medium
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
|
||||
@if (($severityCounts['low'] ?? 0) > 0)
|
||||
<x-filament::badge color="gray" size="sm">
|
||||
{{ $severityCounts['low'] }} low
|
||||
<x-filament::badge color="gray">
|
||||
{{ $severityCounts['low'] }} Low
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
@if ($this->getFindingsUrl())
|
||||
<x-filament::link :href="$this->getFindingsUrl()" size="sm">
|
||||
View findings
|
||||
</x-filament::link>
|
||||
@endif
|
||||
@else
|
||||
<x-filament::badge color="success" size="sm">
|
||||
No drift detected
|
||||
</x-filament::badge>
|
||||
|
||||
@if (filled($message))
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">{{ $message }}</div>
|
||||
@endif
|
||||
<x-filament::button
|
||||
:href="$this->getFindingsUrl()"
|
||||
tag="a"
|
||||
color="gray"
|
||||
icon="heroicon-o-eye"
|
||||
size="sm"
|
||||
>
|
||||
View all findings
|
||||
</x-filament::button>
|
||||
@endif
|
||||
|
||||
@if ($this->getRunUrl())
|
||||
<x-filament::link :href="$this->getRunUrl()" size="sm">
|
||||
View last run
|
||||
</x-filament::link>
|
||||
<x-filament::button
|
||||
:href="$this->getRunUrl()"
|
||||
tag="a"
|
||||
color="gray"
|
||||
outlined
|
||||
icon="heroicon-o-queue-list"
|
||||
size="sm"
|
||||
>
|
||||
Review last run
|
||||
</x-filament::button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
{{-- Ready: no drift --}}
|
||||
@if ($state === 'ready' && ($findingsCount ?? 0) === 0)
|
||||
<x-filament::section>
|
||||
<div class="flex flex-col items-center justify-center gap-3 py-6 text-center">
|
||||
<x-heroicon-o-check-circle class="h-12 w-12 text-success-500" />
|
||||
<div class="text-lg font-semibold text-gray-950 dark:text-white">No Drift Detected</div>
|
||||
<div class="max-w-md text-sm text-gray-500 dark:text-gray-400">
|
||||
The tenant configuration matches the baseline profile. Everything looks good.
|
||||
</div>
|
||||
@if ($this->getRunUrl())
|
||||
<x-filament::button
|
||||
:href="$this->getRunUrl()"
|
||||
tag="a"
|
||||
color="gray"
|
||||
outlined
|
||||
icon="heroicon-o-queue-list"
|
||||
size="sm"
|
||||
>
|
||||
Review last run
|
||||
</x-filament::button>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
{{-- Idle state --}}
|
||||
@if ($state === 'idle')
|
||||
<x-filament::section>
|
||||
<div class="flex flex-col items-center justify-center gap-3 py-6 text-center">
|
||||
<x-heroicon-o-play class="h-12 w-12 text-gray-400 dark:text-gray-500" />
|
||||
<div class="text-lg font-semibold text-gray-950 dark:text-white">Ready to Compare</div>
|
||||
<div class="max-w-md text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ $message }}
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
</x-filament::page>
|
||||
|
||||
@ -1,41 +1,68 @@
|
||||
<div class="rounded-lg bg-white p-4 shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="text-base font-semibold text-gray-950 dark:text-white">
|
||||
Soll vs Ist
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-base font-semibold text-gray-950 dark:text-white">Baseline Governance</div>
|
||||
@if ($landingUrl)
|
||||
<x-filament::link :href="$landingUrl" size="sm" icon="heroicon-m-arrow-top-right-on-square" icon-position="after">
|
||||
Details
|
||||
</x-filament::link>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if (! $hasAssignment)
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
No baseline profile assigned yet.
|
||||
<div class="flex items-start gap-3 rounded-lg bg-gray-50 p-4 dark:bg-white/5">
|
||||
<x-heroicon-o-link-slash class="mt-0.5 h-5 w-5 shrink-0 text-gray-400 dark:text-gray-500" />
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-950 dark:text-white">No Baseline Assigned</div>
|
||||
<div class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">Assign a baseline profile to start monitoring drift.</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
Baseline: <span class="font-medium">{{ $profileName }}</span>
|
||||
{{-- Profile + last compared --}}
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<div class="text-gray-600 dark:text-gray-300">
|
||||
Baseline: <span class="font-medium text-gray-950 dark:text-white">{{ $profileName }}</span>
|
||||
</div>
|
||||
@if ($lastComparedAt)
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ $lastComparedAt }}</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Findings summary --}}
|
||||
@if ($findingsCount > 0)
|
||||
{{-- Critical banner (inline) --}}
|
||||
@if ($highCount > 0)
|
||||
<div class="flex items-center gap-2 rounded-lg border border-danger-300 bg-danger-50 px-3 py-2 dark:border-danger-700 dark:bg-danger-950/50">
|
||||
<x-heroicon-s-exclamation-triangle class="h-4 w-4 shrink-0 text-danger-600 dark:text-danger-400" />
|
||||
<span class="text-sm font-medium text-danger-800 dark:text-danger-200">
|
||||
{{ $highCount }} high-severity {{ Str::plural('finding', $highCount) }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<x-filament::badge color="danger" size="sm">
|
||||
{{ $findingsCount }} open {{ Str::plural('finding', $findingsCount) }}
|
||||
{{ $findingsCount }} {{ Str::plural('finding', $findingsCount) }}
|
||||
</x-filament::badge>
|
||||
|
||||
@if ($highCount > 0)
|
||||
<x-filament::badge color="danger" size="sm">
|
||||
{{ $highCount }} high
|
||||
@if ($mediumCount > 0)
|
||||
<x-filament::badge color="warning" size="sm">
|
||||
{{ $mediumCount }} medium
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
|
||||
@if ($lowCount > 0)
|
||||
<x-filament::badge color="gray" size="sm">
|
||||
{{ $lowCount }} low
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<x-filament::badge color="success" size="sm">
|
||||
No open drift
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
|
||||
@if ($landingUrl)
|
||||
<x-filament::link :href="$landingUrl" size="sm">
|
||||
Go to Soll vs Ist
|
||||
</x-filament::link>
|
||||
<div class="flex items-center gap-2 rounded-lg bg-success-50 px-3 py-2 dark:bg-success-950/50">
|
||||
<x-heroicon-o-check-circle class="h-4 w-4 shrink-0 text-success-600 dark:text-success-400" />
|
||||
<span class="text-sm font-medium text-success-700 dark:text-success-300">No open drift — baseline compliant</span>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user