Implements Spec 084 (verification-surfaces-unification). Highlights - Unifies tenant + onboarding verification start on `provider.connection.check` (OperationRun-based, enqueue-only). - Ensures completed blocked runs persist a schema-valid `context.verification_report` stub (DB-only viewers never show “unavailable”). - Adds tenant embedded verification report widget with DB-only rendering + canonical tenantless “View run” links. - Enforces 404/403 semantics for tenantless run viewing (workspace membership + tenant entitlement required; otherwise 404). - Fixes admin panel widgets to resolve tenant from record context so Owners can start verification and recent operations renders correctly. Tests - Ran: `vendor/bin/sail artisan test --compact tests/Feature/Verification/ tests/Feature/ProviderConnections/ProviderOperationBlockedGuidanceSpec081Test.php tests/Feature/Onboarding/OnboardingVerificationTest.php tests/Feature/RunAuthorizationTenantIsolationTest.php tests/Feature/Filament/TenantVerificationReportWidgetTest.php tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php` Notes - Filament v5 / Livewire v4 compatible. - No new assets; no changes to provider registration. Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box> Reviewed-on: #102
224 lines
6.7 KiB
PHP
224 lines
6.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\Widgets\Tenant;
|
|
|
|
use App\Filament\Support\VerificationReportViewer;
|
|
use App\Models\OperationRun;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Services\Verification\StartVerification;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\Auth\UiTooltips;
|
|
use App\Support\OperationRunLinks;
|
|
use App\Support\OperationRunStatus;
|
|
use Filament\Actions\Action;
|
|
use Filament\Facades\Filament;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Widgets\Widget;
|
|
|
|
class TenantVerificationReport extends Widget
|
|
{
|
|
protected static bool $isLazy = false;
|
|
|
|
protected string $view = 'filament.widgets.tenant.tenant-verification-report';
|
|
|
|
public ?Tenant $record = null;
|
|
|
|
private function resolveTenant(): ?Tenant
|
|
{
|
|
$tenant = Filament::getTenant();
|
|
|
|
if ($tenant instanceof Tenant) {
|
|
return $tenant;
|
|
}
|
|
|
|
return $this->record instanceof Tenant ? $this->record : null;
|
|
}
|
|
|
|
public function startVerification(StartVerification $verification): void
|
|
{
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
abort(403);
|
|
}
|
|
|
|
$tenant = $this->resolveTenant();
|
|
|
|
if (! $tenant instanceof Tenant) {
|
|
abort(404);
|
|
}
|
|
|
|
if (! $user->canAccessTenant($tenant)) {
|
|
abort(404);
|
|
}
|
|
|
|
$result = $verification->providerConnectionCheckForTenant(
|
|
tenant: $tenant,
|
|
initiator: $user,
|
|
extraContext: [
|
|
'surface' => [
|
|
'kind' => 'tenant_verification_widget',
|
|
],
|
|
],
|
|
);
|
|
|
|
$runUrl = OperationRunLinks::tenantlessView($result->run);
|
|
|
|
if ($result->status === 'scope_busy') {
|
|
Notification::make()
|
|
->title('Another operation is already running')
|
|
->body('Please wait for the active run to finish.')
|
|
->warning()
|
|
->actions([
|
|
Action::make('view_run')
|
|
->label('View run')
|
|
->url($runUrl),
|
|
])
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
if ($result->status === 'deduped') {
|
|
Notification::make()
|
|
->title('Verification already running')
|
|
->body('A verification run is already queued or running.')
|
|
->warning()
|
|
->actions([
|
|
Action::make('view_run')
|
|
->label('View run')
|
|
->url($runUrl),
|
|
])
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
if ($result->status === 'blocked') {
|
|
$reasonCode = is_string($result->run->context['reason_code'] ?? null)
|
|
? (string) $result->run->context['reason_code']
|
|
: 'unknown_error';
|
|
|
|
$actions = [
|
|
Action::make('view_run')
|
|
->label('View run')
|
|
->url($runUrl),
|
|
];
|
|
|
|
$nextSteps = $result->run->context['next_steps'] ?? [];
|
|
$nextSteps = is_array($nextSteps) ? $nextSteps : [];
|
|
|
|
foreach ($nextSteps as $index => $step) {
|
|
if (! is_array($step)) {
|
|
continue;
|
|
}
|
|
|
|
$label = is_string($step['label'] ?? null) ? trim((string) $step['label']) : '';
|
|
$url = is_string($step['url'] ?? null) ? trim((string) $step['url']) : '';
|
|
|
|
if ($label === '' || $url === '') {
|
|
continue;
|
|
}
|
|
|
|
$actions[] = Action::make('next_step_'.$index)
|
|
->label($label)
|
|
->url($url);
|
|
|
|
break;
|
|
}
|
|
|
|
Notification::make()
|
|
->title('Verification blocked')
|
|
->body("Blocked by provider configuration ({$reasonCode}).")
|
|
->warning()
|
|
->actions($actions)
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
Notification::make()
|
|
->title('Verification started')
|
|
->success()
|
|
->actions([
|
|
Action::make('view_run')
|
|
->label('View run')
|
|
->url($runUrl),
|
|
])
|
|
->send();
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
protected function getViewData(): array
|
|
{
|
|
$tenant = $this->resolveTenant();
|
|
|
|
if (! $tenant instanceof Tenant) {
|
|
return [
|
|
'tenant' => null,
|
|
'run' => null,
|
|
'runData' => null,
|
|
'runUrl' => null,
|
|
'report' => null,
|
|
'isInProgress' => false,
|
|
'canStart' => false,
|
|
'startTooltip' => null,
|
|
];
|
|
}
|
|
|
|
$run = OperationRun::query()
|
|
->where('tenant_id', (int) $tenant->getKey())
|
|
->where('type', 'provider.connection.check')
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
$report = $run instanceof OperationRun
|
|
? VerificationReportViewer::report($run)
|
|
: null;
|
|
|
|
$isInProgress = $run instanceof OperationRun
|
|
&& (string) $run->status !== OperationRunStatus::Completed->value;
|
|
|
|
$user = auth()->user();
|
|
$isTenantMember = $user instanceof User && $user->canAccessTenant($tenant);
|
|
$canStart = $isTenantMember
|
|
&& $user->can(Capabilities::PROVIDER_RUN, $tenant);
|
|
|
|
$runData = null;
|
|
|
|
if ($run instanceof OperationRun) {
|
|
$context = is_array($run->context ?? null) ? $run->context : [];
|
|
$targetScope = $context['target_scope'] ?? [];
|
|
$targetScope = is_array($targetScope) ? $targetScope : [];
|
|
|
|
$runData = [
|
|
'id' => (int) $run->getKey(),
|
|
'type' => (string) $run->type,
|
|
'status' => (string) $run->status,
|
|
'outcome' => (string) $run->outcome,
|
|
'initiator_name' => (string) $run->initiator_name,
|
|
'started_at' => $run->started_at?->toJSON(),
|
|
'completed_at' => $run->completed_at?->toJSON(),
|
|
'target_scope' => $targetScope,
|
|
'failures' => is_array($run->failure_summary ?? null) ? $run->failure_summary : [],
|
|
];
|
|
}
|
|
|
|
return [
|
|
'tenant' => $tenant,
|
|
'run' => $run,
|
|
'runData' => $runData,
|
|
'runUrl' => $run instanceof OperationRun ? OperationRunLinks::tenantlessView($run) : null,
|
|
'report' => $report,
|
|
'isInProgress' => $isInProgress,
|
|
'canStart' => $canStart,
|
|
'startTooltip' => $isTenantMember && ! $canStart ? UiTooltips::insufficientPermission() : null,
|
|
];
|
|
}
|
|
}
|