TenantAtlas/app/Filament/Pages/BaselineCompareLanding.php

278 lines
8.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Filament\Pages;
use App\Filament\Resources\FindingResource;
use App\Models\BaselineTenantAssignment;
use App\Models\Finding;
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Models\User;
use App\Services\Auth\CapabilityResolver;
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;
use Filament\Pages\Page;
use UnitEnum;
class BaselineCompareLanding extends Page
{
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-scale';
protected static string|UnitEnum|null $navigationGroup = 'Governance';
protected static ?string $navigationLabel = 'Soll vs Ist';
protected static ?int $navigationSort = 30;
protected string $view = 'filament.pages.baseline-compare-landing';
public ?string $state = null;
public ?string $message = null;
public ?string $profileName = null;
public ?int $profileId = null;
public ?int $snapshotId = null;
public ?int $operationRunId = null;
public ?int $findingsCount = null;
/** @var array<string, int>|null */
public ?array $severityCounts = null;
public static function canAccess(): bool
{
$user = auth()->user();
if (! $user instanceof User) {
return false;
}
$tenant = Tenant::current();
if (! $tenant instanceof Tenant) {
return false;
}
$resolver = app(CapabilityResolver::class);
return $resolver->can($user, $tenant, Capabilities::TENANT_VIEW);
}
public function mount(): void
{
$tenant = Tenant::current();
if (! $tenant instanceof Tenant) {
$this->state = 'no_tenant';
$this->message = 'No tenant selected.';
return;
}
$assignment = BaselineTenantAssignment::query()
->where('tenant_id', $tenant->getKey())
->first();
if (! $assignment instanceof BaselineTenantAssignment) {
$this->state = 'no_assignment';
$this->message = 'This tenant has no baseline assignment. A workspace manager can assign a baseline profile to this tenant.';
return;
}
$profile = $assignment->baselineProfile;
if ($profile === null) {
$this->state = 'no_assignment';
$this->message = 'The assigned baseline profile no longer exists.';
return;
}
$this->profileName = (string) $profile->name;
$this->profileId = (int) $profile->getKey();
$this->snapshotId = $profile->active_snapshot_id !== null ? (int) $profile->active_snapshot_id : null;
if ($this->snapshotId === null) {
$this->state = 'no_snapshot';
$this->message = 'The baseline profile has no active snapshot yet. A workspace manager needs to capture a snapshot first.';
return;
}
$latestRun = OperationRun::query()
->where('tenant_id', $tenant->getKey())
->where('type', 'baseline_compare')
->latest('id')
->first();
if ($latestRun instanceof OperationRun && in_array($latestRun->status, ['queued', 'running'], true)) {
$this->state = 'comparing';
$this->operationRunId = (int) $latestRun->getKey();
$this->message = 'A baseline comparison is currently in progress.';
return;
}
$scopeKey = 'baseline_profile:' . $profile->getKey();
$findingsQuery = Finding::query()
->where('tenant_id', $tenant->getKey())
->where('finding_type', Finding::FINDING_TYPE_DRIFT)
->where('source', 'baseline.compare')
->where('scope_key', $scopeKey);
$totalFindings = (int) (clone $findingsQuery)->count();
if ($totalFindings > 0) {
$this->state = 'ready';
$this->findingsCount = $totalFindings;
$this->severityCounts = [
'high' => (int) (clone $findingsQuery)->where('severity', Finding::SEVERITY_HIGH)->count(),
'medium' => (int) (clone $findingsQuery)->where('severity', Finding::SEVERITY_MEDIUM)->count(),
'low' => (int) (clone $findingsQuery)->where('severity', Finding::SEVERITY_LOW)->count(),
];
if ($latestRun instanceof OperationRun) {
$this->operationRunId = (int) $latestRun->getKey();
}
return;
}
if ($latestRun instanceof OperationRun && $latestRun->status === 'completed' && $latestRun->outcome === 'succeeded') {
$this->state = 'ready';
$this->findingsCount = 0;
$this->operationRunId = (int) $latestRun->getKey();
$this->message = 'No drift findings for this baseline comparison. The tenant matches the baseline.';
return;
}
$this->state = 'idle';
$this->message = 'Baseline profile is assigned and has a snapshot. Run "Compare Now" to check for drift.';
}
/**
* @return array<Action>
*/
protected function getHeaderActions(): array
{
return [
$this->compareNowAction(),
];
}
private function compareNowAction(): Action
{
return Action::make('compareNow')
->label('Compare Now')
->icon('heroicon-o-play')
->requiresConfirmation()
->modalHeading('Start baseline comparison')
->modalDescription('This will compare the current tenant inventory against the assigned baseline snapshot and generate drift findings.')
->visible(fn (): bool => $this->canCompare())
->disabled(fn (): bool => ! in_array($this->state, ['idle', 'ready'], true))
->action(function (): void {
$user = auth()->user();
if (! $user instanceof User) {
Notification::make()->title('Not authenticated')->danger()->send();
return;
}
$tenant = Tenant::current();
if (! $tenant instanceof Tenant) {
Notification::make()->title('No tenant context')->danger()->send();
return;
}
$service = app(BaselineCompareService::class);
$result = $service->startCompare($tenant, $user);
if (! ($result['ok'] ?? false)) {
Notification::make()
->title('Cannot start comparison')
->body('Reason: ' . ($result['reason_code'] ?? 'unknown'))
->danger()
->send();
return;
}
$run = $result['run'] ?? null;
if ($run instanceof OperationRun) {
$this->operationRunId = (int) $run->getKey();
}
$this->state = 'comparing';
Notification::make()
->title('Baseline comparison started')
->body('A background job will compute drift against the baseline snapshot.')
->success()
->send();
});
}
private function canCompare(): bool
{
$user = auth()->user();
if (! $user instanceof User) {
return false;
}
$tenant = Tenant::current();
if (! $tenant instanceof Tenant) {
return false;
}
$resolver = app(CapabilityResolver::class);
return $resolver->can($user, $tenant, Capabilities::TENANT_SYNC);
}
public function getFindingsUrl(): ?string
{
$tenant = Tenant::current();
if (! $tenant instanceof Tenant) {
return null;
}
return FindingResource::getUrl('index', tenant: $tenant);
}
public function getRunUrl(): ?string
{
if ($this->operationRunId === null) {
return null;
}
$tenant = Tenant::current();
if (! $tenant instanceof Tenant) {
return null;
}
return OperationRunLinks::view($this->operationRunId, $tenant);
}
}