feat(spec-078): canonical tenantless operations detail
This commit is contained in:
parent
94d5eab217
commit
5e104101b6
@ -7,9 +7,12 @@ Thumbs.db
|
||||
.env
|
||||
.env.*
|
||||
*.log
|
||||
*.log*
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
Dockerfile*
|
||||
.dockerignore
|
||||
*.tmp
|
||||
*.swp
|
||||
public/build/
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,6 +6,7 @@
|
||||
.env.production
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
*.cache
|
||||
/.fleet
|
||||
/.idea
|
||||
/.nova
|
||||
@ -24,6 +25,7 @@ coverage/
|
||||
/storage/pail
|
||||
/storage/framework
|
||||
/storage/logs
|
||||
/storage/debugbar
|
||||
/vendor
|
||||
/bootstrap/cache
|
||||
Homestead.json
|
||||
|
||||
@ -4,18 +4,22 @@
|
||||
|
||||
namespace App\Filament\Pages\Operations;
|
||||
|
||||
use App\Filament\Resources\OperationRunResource;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Models\WorkspaceMembership;
|
||||
use App\Services\Auth\CapabilityResolver;
|
||||
use App\Support\OperationRunLinks;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Schemas\Components\EmbeddedSchema;
|
||||
use Filament\Schemas\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class TenantlessOperationRunViewer extends Page
|
||||
{
|
||||
protected static string $layout = 'filament-panels::components.layout.simple';
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
protected static bool $isDiscovered = false;
|
||||
@ -26,8 +30,10 @@ class TenantlessOperationRunViewer extends Page
|
||||
|
||||
public OperationRun $run;
|
||||
|
||||
public bool $opsUxIsTabHidden = false;
|
||||
|
||||
/**
|
||||
* @return array<Action>
|
||||
* @return array<Action|ActionGroup>
|
||||
*/
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
@ -36,32 +42,39 @@ protected function getHeaderActions(): array
|
||||
->label('Refresh')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->color('gray')
|
||||
->url(fn (): string => url()->current()),
|
||||
->url(fn (): string => isset($this->run)
|
||||
? route('admin.operations.view', ['run' => (int) $this->run->getKey()])
|
||||
: route('admin.operations.index')),
|
||||
];
|
||||
|
||||
if (! isset($this->run)) {
|
||||
return $actions;
|
||||
}
|
||||
|
||||
$tenant = $this->run->tenant;
|
||||
$user = auth()->user();
|
||||
$tenant = $this->run->tenant;
|
||||
|
||||
if (! $tenant instanceof Tenant || ! $user instanceof User) {
|
||||
return $actions;
|
||||
if ($tenant instanceof Tenant && (! $user instanceof User || ! app(CapabilityResolver::class)->isMember($user, $tenant))) {
|
||||
$tenant = null;
|
||||
}
|
||||
|
||||
if (! app(CapabilityResolver::class)->isMember($user, $tenant)) {
|
||||
return $actions;
|
||||
$related = OperationRunLinks::related($this->run, $tenant);
|
||||
|
||||
$relatedActions = [];
|
||||
|
||||
foreach ($related as $label => $url) {
|
||||
$relatedActions[] = Action::make(Str::slug((string) $label, '_'))
|
||||
->label((string) $label)
|
||||
->url((string) $url)
|
||||
->openUrlInNewTab();
|
||||
}
|
||||
|
||||
$actions[] = Action::make('admin_details')
|
||||
->label('Admin details')
|
||||
->icon('heroicon-o-arrow-top-right-on-square')
|
||||
->color('gray')
|
||||
->url(fn (): string => route('filament.admin.resources.operations.view', [
|
||||
'tenant' => (int) $tenant->getKey(),
|
||||
'record' => (int) $this->run->getKey(),
|
||||
]));
|
||||
if ($relatedActions !== []) {
|
||||
$actions[] = ActionGroup::make($relatedActions)
|
||||
->label('Open')
|
||||
->icon('heroicon-o-arrow-top-right-on-square')
|
||||
->color('gray');
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
@ -91,4 +104,23 @@ public function mount(OperationRun $run): void
|
||||
|
||||
$this->run = $run->loadMissing(['workspace', 'tenant', 'user']);
|
||||
}
|
||||
|
||||
public function infolist(Schema $schema): Schema
|
||||
{
|
||||
return OperationRunResource::infolist($schema);
|
||||
}
|
||||
|
||||
public function defaultInfolist(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->record($this->run)
|
||||
->columns(2);
|
||||
}
|
||||
|
||||
public function content(Schema $schema): Schema
|
||||
{
|
||||
return $schema->schema([
|
||||
EmbeddedSchema::make('infolist'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\OperationRunResource\Pages;
|
||||
use App\Filament\Support\VerificationReportChangeIndicator;
|
||||
use App\Filament\Support\VerificationReportViewer;
|
||||
use App\Models\OperationRun;
|
||||
@ -91,6 +90,11 @@ public static function infolist(Schema $schema): Schema
|
||||
->getStateUsing(fn (OperationRun $record): ?string => static::targetScopeDisplay($record))
|
||||
->visible(fn (OperationRun $record): bool => static::targetScopeDisplay($record) !== null)
|
||||
->columnSpanFull(),
|
||||
TextEntry::make('target_scope_empty_state')
|
||||
->label('Target')
|
||||
->getStateUsing(static fn (): string => 'No target scope details were recorded for this run.')
|
||||
->visible(fn (OperationRun $record): bool => static::targetScopeDisplay($record) === null)
|
||||
->columnSpanFull(),
|
||||
TextEntry::make('elapsed')
|
||||
->label('Elapsed')
|
||||
->getStateUsing(fn (OperationRun $record): string => RunDurationInsights::elapsedHuman($record)),
|
||||
@ -385,6 +389,7 @@ public static function table(Table $table): Table
|
||||
])
|
||||
->actions([
|
||||
Actions\ViewAction::make()
|
||||
->label('View run')
|
||||
->url(fn (OperationRun $record): string => route('admin.operations.view', ['run' => (int) $record->getKey()])),
|
||||
])
|
||||
->bulkActions([]);
|
||||
@ -392,10 +397,7 @@ public static function table(Table $table): Table
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListOperationRuns::route('/'),
|
||||
'view' => Pages\ViewOperationRun::route('/r/{record}'),
|
||||
];
|
||||
return [];
|
||||
}
|
||||
|
||||
private static function targetScopeDisplay(OperationRun $record): ?string
|
||||
|
||||
@ -1,64 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\OperationRunResource\Pages;
|
||||
|
||||
use App\Filament\Resources\OperationRunResource;
|
||||
use App\Filament\Widgets\Operations\OperationsKpiHeader;
|
||||
use App\Models\Tenant;
|
||||
use App\Support\OperationRunOutcome;
|
||||
use App\Support\OperationRunStatus;
|
||||
use App\Support\OpsUx\ActiveRuns;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ListOperationRuns extends ListRecords
|
||||
{
|
||||
protected static string $resource = OperationRunResource::class;
|
||||
|
||||
protected function getHeaderWidgets(): array
|
||||
{
|
||||
return [
|
||||
OperationsKpiHeader::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, Tab>
|
||||
*/
|
||||
public function getTabs(): array
|
||||
{
|
||||
return [
|
||||
'all' => Tab::make(),
|
||||
'active' => Tab::make()
|
||||
->modifyQueryUsing(fn (Builder $query): Builder => $query->whereIn('status', [
|
||||
OperationRunStatus::Queued->value,
|
||||
OperationRunStatus::Running->value,
|
||||
])),
|
||||
'succeeded' => Tab::make()
|
||||
->modifyQueryUsing(fn (Builder $query): Builder => $query
|
||||
->where('status', OperationRunStatus::Completed->value)
|
||||
->where('outcome', OperationRunOutcome::Succeeded->value)),
|
||||
'partial' => Tab::make()
|
||||
->modifyQueryUsing(fn (Builder $query): Builder => $query
|
||||
->where('status', OperationRunStatus::Completed->value)
|
||||
->where('outcome', OperationRunOutcome::PartiallySucceeded->value)),
|
||||
'failed' => Tab::make()
|
||||
->modifyQueryUsing(fn (Builder $query): Builder => $query
|
||||
->where('status', OperationRunStatus::Completed->value)
|
||||
->where('outcome', OperationRunOutcome::Failed->value)),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getTablePollingInterval(): ?string
|
||||
{
|
||||
$tenant = Filament::getTenant();
|
||||
|
||||
if (! $tenant instanceof Tenant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ActiveRuns::existForTenant($tenant) ? '10s' : null;
|
||||
}
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\OperationRunResource\Pages;
|
||||
|
||||
use App\Filament\Resources\OperationRunResource;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\Tenant;
|
||||
use App\Support\OperationRunLinks;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ViewOperationRun extends ViewRecord
|
||||
{
|
||||
protected static string $resource = OperationRunResource::class;
|
||||
|
||||
public bool $opsUxIsTabHidden = false;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
$tenant = Tenant::current();
|
||||
|
||||
if (! $tenant instanceof Tenant) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @var OperationRun $run */
|
||||
$run = $this->getRecord();
|
||||
|
||||
$related = OperationRunLinks::related($run, $tenant);
|
||||
|
||||
$actions = [];
|
||||
|
||||
foreach ($related as $label => $url) {
|
||||
$actions[] = Actions\Action::make(Str::slug($label, '_'))
|
||||
->label($label)
|
||||
->url($url)
|
||||
->openUrlInNewTab();
|
||||
}
|
||||
|
||||
if (empty($actions)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
Actions\ActionGroup::make($actions)
|
||||
->label('Open')
|
||||
->icon('heroicon-o-arrow-top-right-on-square')
|
||||
->color('gray'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -40,12 +40,7 @@ protected function getStats(): array
|
||||
$tenant = Filament::getTenant();
|
||||
|
||||
if (! $tenant instanceof Tenant) {
|
||||
return [
|
||||
Stat::make('Total Runs (30 days)', 0),
|
||||
Stat::make('Active Runs', 0),
|
||||
Stat::make('Failed/Partial (7 days)', 0),
|
||||
Stat::make('Avg Duration (7 days)', '—'),
|
||||
];
|
||||
return [];
|
||||
}
|
||||
|
||||
$tenantId = (int) $tenant->getKey();
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Monitoring;
|
||||
|
||||
use App\Models\OperationRun;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Livewire\Component;
|
||||
|
||||
class OperationsDetail extends Component implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
public OperationRun $run;
|
||||
|
||||
public function mount(OperationRun $run): void
|
||||
{
|
||||
// Ensure tenant scope
|
||||
abort_unless($run->tenant_id === filament()->getTenant()->id, 403);
|
||||
$this->run = $run;
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.monitoring.operations-detail');
|
||||
}
|
||||
}
|
||||
@ -15,7 +15,7 @@
|
||||
|
||||
final class OperationRunLinks
|
||||
{
|
||||
public static function index(Tenant $tenant): string
|
||||
public static function index(?Tenant $tenant = null): string
|
||||
{
|
||||
return route('admin.operations.index');
|
||||
}
|
||||
@ -35,7 +35,7 @@ public static function view(OperationRun|int $run, Tenant $tenant): string
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function related(OperationRun $run, Tenant $tenant): array
|
||||
public static function related(OperationRun $run, ?Tenant $tenant): array
|
||||
{
|
||||
$context = is_array($run->context) ? $run->context : [];
|
||||
|
||||
@ -43,6 +43,10 @@ public static function related(OperationRun $run, Tenant $tenant): array
|
||||
|
||||
$links['Operations'] = self::index($tenant);
|
||||
|
||||
if (! $tenant instanceof Tenant) {
|
||||
return $links;
|
||||
}
|
||||
|
||||
$providerConnectionId = $context['provider_connection_id'] ?? null;
|
||||
|
||||
if (is_numeric($providerConnectionId) && class_exists(ProviderConnectionResource::class)) {
|
||||
|
||||
@ -1,137 +1,3 @@
|
||||
<x-filament-panels::page>
|
||||
@php
|
||||
$context = is_array($this->run->context ?? null) ? $this->run->context : [];
|
||||
$targetScope = $context['target_scope'] ?? [];
|
||||
$targetScope = is_array($targetScope) ? $targetScope : [];
|
||||
|
||||
$failures = is_array($this->run->failure_summary ?? null) ? $this->run->failure_summary : [];
|
||||
@endphp
|
||||
|
||||
<div class="space-y-6">
|
||||
<x-filament::section heading="Summary">
|
||||
<div class="grid grid-cols-1 gap-3 text-sm text-gray-700 dark:text-gray-200 md:grid-cols-2">
|
||||
<div>
|
||||
<span class="text-gray-500 dark:text-gray-400">Run ID:</span>
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100">{{ (int) $this->run->getKey() }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-gray-500 dark:text-gray-400">Workspace:</span>
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100">{{ (string) ($this->run->workspace?->name ?? '—') }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-gray-500 dark:text-gray-400">Operation:</span>
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100">{{ (string) $this->run->type }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-gray-500 dark:text-gray-400">Initiator:</span>
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100">{{ (string) $this->run->initiator_name }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-gray-500 dark:text-gray-400">Status:</span>
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100">{{ (string) $this->run->status }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-gray-500 dark:text-gray-400">Outcome:</span>
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100">{{ (string) $this->run->outcome }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-gray-500 dark:text-gray-400">Started:</span>
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100">{{ $this->run->started_at?->format('Y-m-d H:i') ?? '—' }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-gray-500 dark:text-gray-400">Completed:</span>
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100">{{ $this->run->completed_at?->format('Y-m-d H:i') ?? '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
<x-filament::section heading="Target scope" :collapsed="false">
|
||||
@php
|
||||
$entraTenantId = $targetScope['entra_tenant_id'] ?? null;
|
||||
$entraTenantName = $targetScope['entra_tenant_name'] ?? null;
|
||||
|
||||
$entraTenantId = is_string($entraTenantId) && $entraTenantId !== '' ? $entraTenantId : null;
|
||||
$entraTenantName = is_string($entraTenantName) && $entraTenantName !== '' ? $entraTenantName : null;
|
||||
@endphp
|
||||
|
||||
@if ($entraTenantId === null && $entraTenantName === null)
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
No target scope details were recorded for this run.
|
||||
</div>
|
||||
@else
|
||||
<div class="flex flex-col gap-2 text-sm text-gray-700 dark:text-gray-200">
|
||||
@if ($entraTenantName !== null)
|
||||
<div>
|
||||
<span class="text-gray-500 dark:text-gray-400">Entra tenant:</span>
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100">{{ $entraTenantName }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($entraTenantId !== null)
|
||||
<div>
|
||||
<span class="text-gray-500 dark:text-gray-400">Entra tenant ID:</span>
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100">{{ $entraTenantId }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</x-filament::section>
|
||||
|
||||
<x-filament::section heading="Report">
|
||||
@if ((string) $this->run->status !== 'completed')
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
Report unavailable while the run is in progress. Use “Refresh” to re-check stored status.
|
||||
</div>
|
||||
@elseif ((string) $this->run->outcome === 'succeeded')
|
||||
<div class="text-sm text-gray-700 dark:text-gray-200">
|
||||
No failures were reported.
|
||||
</div>
|
||||
@elseif ($failures === [])
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
Report unavailable. The run completed, but no failure details were recorded.
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-3">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
Findings
|
||||
</div>
|
||||
|
||||
<ul class="space-y-2 text-sm text-gray-700 dark:text-gray-200">
|
||||
@foreach ($failures as $failure)
|
||||
@php
|
||||
$reasonCode = is_array($failure) ? ($failure['reason_code'] ?? null) : null;
|
||||
$message = is_array($failure) ? ($failure['message'] ?? null) : null;
|
||||
|
||||
$reasonCode = is_string($reasonCode) && $reasonCode !== '' ? $reasonCode : null;
|
||||
$message = is_string($message) && $message !== '' ? $message : null;
|
||||
@endphp
|
||||
|
||||
@if ($reasonCode !== null || $message !== null)
|
||||
<li class="rounded-lg border border-gray-200 bg-white p-3 dark:border-gray-800 dark:bg-gray-900">
|
||||
@if ($reasonCode !== null)
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
{{ $reasonCode }}
|
||||
</div>
|
||||
@endif
|
||||
@if ($message !== null)
|
||||
<div class="mt-1 text-sm text-gray-700 dark:text-gray-200">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@endif
|
||||
</li>
|
||||
@endif
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
</x-filament::section>
|
||||
</div>
|
||||
{{ $this->infolist }}
|
||||
</x-filament-panels::page>
|
||||
|
||||
|
||||
@ -1,60 +0,0 @@
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="bg-white p-4 rounded shadow">
|
||||
<h3 class="font-bold text-lg mb-2">Summary</h3>
|
||||
<dl class="grid grid-cols-2 gap-x-4 gap-y-2">
|
||||
<dt class="text-gray-600">Type:</dt>
|
||||
<dd>{{ $run->type }}</dd>
|
||||
|
||||
<dt class="text-gray-600">Status:</dt>
|
||||
<dd>{{ $run->status }}</dd>
|
||||
|
||||
<dt class="text-gray-600">Outcome:</dt>
|
||||
<dd>{{ $run->outcome }}</dd>
|
||||
|
||||
<dt class="text-gray-600">Initiator:</dt>
|
||||
<dd>{{ $run->initiator_name }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-4 rounded shadow">
|
||||
<h3 class="font-bold text-lg mb-2">Timing</h3>
|
||||
<dl class="grid grid-cols-2 gap-x-4 gap-y-2">
|
||||
<dt class="text-gray-600">Created:</dt>
|
||||
<dd>{{ $run->created_at }}</dd>
|
||||
|
||||
<dt class="text-gray-600">Started:</dt>
|
||||
<dd>{{ $run->started_at ?? '-' }}</dd>
|
||||
|
||||
<dt class="text-gray-600">Completed:</dt>
|
||||
<dd>{{ $run->completed_at ?? '-' }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(!empty($run->summary_counts))
|
||||
<div class="bg-white p-4 rounded shadow">
|
||||
<h3 class="font-bold text-lg mb-2">Counts</h3>
|
||||
<pre class="bg-gray-100 p-2 rounded text-sm overflow-auto">{{ json_encode($run->summary_counts, JSON_PRETTY_PRINT) }}</pre>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(!empty($run->failure_summary))
|
||||
<div class="bg-white p-4 rounded shadow border-l-4 border-red-500">
|
||||
<h3 class="font-bold text-lg mb-2 text-red-700">Failures</h3>
|
||||
<div class="space-y-2">
|
||||
@foreach($run->failure_summary as $failure)
|
||||
<div class="bg-red-50 p-2 rounded">
|
||||
<div class="font-mono text-xs text-red-800">{{ $failure['code'] ?? 'UNKNOWN' }}</div>
|
||||
<div class="text-sm text-red-900">{{ $failure['message'] ?? 'Unknown error' }}</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="bg-white p-4 rounded shadow">
|
||||
<h3 class="font-bold text-lg mb-2">Context</h3>
|
||||
<pre class="bg-gray-100 p-2 rounded text-sm overflow-auto">{{ json_encode($run->context, JSON_PRETTY_PRINT) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
@ -137,6 +137,20 @@
|
||||
->get('/admin/onboarding', \App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class)
|
||||
->name('admin.onboarding');
|
||||
|
||||
Route::middleware([
|
||||
'web',
|
||||
'panel:admin',
|
||||
'ensure-correct-guard:web',
|
||||
DenyNonMemberTenantAccess::class,
|
||||
DisableBladeIconComponents::class,
|
||||
DispatchServingFilamentEvent::class,
|
||||
FilamentAuthenticate::class,
|
||||
'ensure-workspace-selected',
|
||||
'ensure-filament-tenant-selected',
|
||||
])
|
||||
->get('/admin/t/{tenant}/operations', fn () => redirect()->route('admin.operations.index'))
|
||||
->name('admin.operations.legacy-index');
|
||||
|
||||
Route::middleware([
|
||||
'web',
|
||||
'panel:admin',
|
||||
@ -187,6 +201,7 @@
|
||||
DisableBladeIconComponents::class,
|
||||
DispatchServingFilamentEvent::class,
|
||||
FilamentAuthenticate::class,
|
||||
'ensure-filament-tenant-selected',
|
||||
])
|
||||
->get('/admin/operations/{run}', \App\Filament\Pages\Operations\TenantlessOperationRunViewer::class)
|
||||
->name('admin.operations.view');
|
||||
|
||||
@ -33,7 +33,7 @@ ### GET /admin/operations/{run}
|
||||
|
||||
---
|
||||
|
||||
## Decommissioned Routes (Removed After Migration)
|
||||
## Decommissioned Routes (Resource-Generated Routes Removed After Migration)
|
||||
|
||||
### GET /admin/t/{tenant}/operations/r/{record}
|
||||
|
||||
@ -50,7 +50,7 @@ ### GET /admin/t/{tenant}/operations
|
||||
|----------|-------|
|
||||
| **Route name** | `filament.admin.resources.operations.index` |
|
||||
| **Status** | ❌ **REMOVED** — route no longer registered |
|
||||
| **After migration** | Natural 404 (or optional 302 → `/admin/operations` per FR-078-012) |
|
||||
| **After migration** | Replaced by explicit convenience route `admin.operations.legacy-index` that redirects 302 → `/admin/operations` |
|
||||
| **Previously** | `ListOperationRuns` (Filament ListRecords page) |
|
||||
|
||||
---
|
||||
@ -97,6 +97,7 @@ ### Positive (must work)
|
||||
|------|-------|----------|
|
||||
| T-078-001 | `GET /admin/operations/{run}` | 200 (member) |
|
||||
| T-078-001 | `GET /admin/operations/{run}` | 200 (run with `tenant_id = null`) |
|
||||
| T-078-009 | `GET /admin/t/{tenant}/operations` | 302 redirect to `/admin/operations` |
|
||||
|
||||
### Negative (must 404)
|
||||
|
||||
|
||||
@ -46,9 +46,15 @@ ### Routes Removed
|
||||
|
||||
| Route Name | Pattern | Handler |
|
||||
|------------|---------|---------|
|
||||
| `filament.admin.resources.operations.index` | `GET /admin/t/{tenant}/operations` | `ListOperationRuns` |
|
||||
| `filament.admin.resources.operations.index` | `GET /admin/t/{tenant}/operations` | `ListOperationRuns` (resource-generated) |
|
||||
| `filament.admin.resources.operations.view` | `GET /admin/t/{tenant}/operations/r/{record}` | `ViewOperationRun` |
|
||||
|
||||
### Route Added
|
||||
|
||||
| Route Name | Pattern | Handler |
|
||||
|------------|---------|---------|
|
||||
| `admin.operations.legacy-index` | `GET /admin/t/{tenant}/operations` | Redirect 302 to `/admin/operations` |
|
||||
|
||||
### Routes Retained (Unchanged)
|
||||
|
||||
| Route Name | Pattern | Handler |
|
||||
@ -81,7 +87,8 @@ ### Test Files Requiring Updates
|
||||
| `tests/Feature/Verification/VerificationAuthorizationTest.php` | Replace `OperationRunResource::getUrl('view')` with `route('admin.operations.view')` |
|
||||
| `tests/Feature/OpsUx/FailureSanitizationTest.php` | Replace `OperationRunResource::getUrl('view')` with canonical route; replace `ViewOperationRun` mount |
|
||||
| `tests/Feature/OpsUx/CanonicalViewRunLinksTest.php` | Update guard regex to account for headless resource |
|
||||
| `tests/Feature/Verification/VerificationDbOnlyTest.php` | Replace `ViewOperationRun` mount with `TenantlessOperationRunViewer` |
|
||||
| `tests/Feature/Verification/VerificationReportRenderingTest.php` | Replace `ViewOperationRun` mount with `TenantlessOperationRunViewer` |
|
||||
| `tests/Feature/Verification/VerificationReportViewerDbOnlyTest.php` | Replace `ViewOperationRun` mount with `TenantlessOperationRunViewer` |
|
||||
| `tests/Feature/Verification/VerificationReportRedactionTest.php` | Replace `ViewOperationRun` mount with `TenantlessOperationRunViewer` |
|
||||
| `tests/Feature/Verification/VerificationReportMissingOrMalformedTest.php` | Replace `ViewOperationRun` mount with `TenantlessOperationRunViewer` |
|
||||
| `tests/Feature/Monitoring/OperationsCanonicalUrlsTest.php` | Remove `ListOperationRuns` test; add route-not-registered assertion |
|
||||
| `tests/Feature/Monitoring/OperationsTenantScopeTest.php` | Remove `ListOperationRuns` test |
|
||||
|
||||
@ -94,8 +94,9 @@ ### Source Code (files touched)
|
||||
| +-- OperationsTenantScopeTest.php # Remove ListOperationRuns reference
|
||||
+-- Verification/
|
||||
| +-- VerificationAuthorizationTest.php # Canonical route instead of getUrl
|
||||
| +-- VerificationDbOnlyTest.php # TenantlessViewer replaces ViewOperationRun
|
||||
| +-- VerificationReportRenderingTest.php # TenantlessViewer replaces ViewOperationRun
|
||||
| +-- VerificationReportViewerDbOnlyTest.php # TenantlessViewer replaces ViewOperationRun
|
||||
| +-- VerificationReportRedactionTest.php # TenantlessViewer replaces ViewOperationRun
|
||||
| +-- VerificationReportMissingOrMalformedTest.php # TenantlessViewer replaces ViewOperationRun
|
||||
+-- OpsUx/
|
||||
| +-- FailureSanitizationTest.php # Canonical route + TenantlessViewer
|
||||
| +-- CanonicalViewRunLinksTest.php # Update guard regex
|
||||
@ -153,7 +154,7 @@ ### Phase B — Infolist Reuse on TenantlessOperationRunViewer
|
||||
| B.3 | `TenantlessOperationRunViewer.php` | Add `content(Schema $schema)` — returns `EmbeddedSchema::make('infolist')` |
|
||||
| B.4 | `TenantlessOperationRunViewer.php` | Add `public bool $opsUxIsTabHidden = false` property (polling callback) |
|
||||
| B.5 | `tenantless-operation-run-viewer.blade.php` | Replace hand-coded HTML with infolist render tag |
|
||||
| B.6 | `tests/Feature/078/CanonicalDetailRenderTest.php` | New: T-078-001 + T-078-005 |
|
||||
| B.6 | `tests/Feature/078/CanonicalDetailRenderTest.php` | New: T-078-001 (+ T-078-007 guard) |
|
||||
| B.7 | `tests/Feature/078/VerificationReportTenantlessTest.php` | New: T-078-008 |
|
||||
|
||||
**Exit criteria**: Canonical detail shows identical layout to old tenant-scoped view; verification report section renders.
|
||||
@ -168,7 +169,7 @@ ### Phase C — Contextual Navigation (Related Links)
|
||||
|------|------|--------|
|
||||
| C.1 | `TenantlessOperationRunViewer.php` | Add `getHeaderActions()` using `OperationRunLinks::related()` |
|
||||
| C.2 | `TenantlessOperationRunViewer.php` | Remove "Admin details" button code (~line 61) |
|
||||
| C.3 | `tests/Feature/078/RelatedLinksOnDetailTest.php` | New: T-078-010 |
|
||||
| C.3 | `tests/Feature/078/RelatedLinksOnDetailTest.php` | New: T-078-010 + T-078-005 + T-078-012 |
|
||||
|
||||
**Exit criteria**: Related links render for different run types; "Admin details" link absent.
|
||||
|
||||
@ -176,16 +177,17 @@ ### Phase D — KPI Header Tenantless Handling
|
||||
|
||||
**Goal**: Hide KPI stats when no tenant context (not workspace-scoped — deferred).
|
||||
**Risk**: Low — conditional early return.
|
||||
**Tests first**: T-078-006 (KPI hidden in tenantless mode).
|
||||
**Tests first**: T-078-006 (KPI hidden in tenantless mode), T-078-011 (tenantless list query safety).
|
||||
|
||||
| Step | File | Change |
|
||||
|------|------|--------|
|
||||
| D.1 | `OperationsKpiHeader.php` | `getStats()`: if `Filament::getTenant()` is null then return `[]` |
|
||||
| D.2 | `tests/Feature/078/KpiHeaderTenantlessTest.php` | New: T-078-006 |
|
||||
| D.3 | `tests/Feature/078/OperationsListTenantlessSafetyTest.php` | New: T-078-011 |
|
||||
|
||||
**Exit criteria**: Operations page without tenant context renders without errors; KPI section hidden.
|
||||
|
||||
### Phase E — Optional List Redirect (FR-078-012)
|
||||
### Phase E — List Redirect (FR-078-012)
|
||||
|
||||
**Goal**: Convenience redirect for decommissioned list URL.
|
||||
**Risk**: Low — single route addition.
|
||||
@ -248,11 +250,13 @@ ### New Tests (spec-specific in tests/Feature/078/)
|
||||
| T-078-001 | CanonicalDetailRenderTest.php | Detail renders with/without tenant_id |
|
||||
| T-078-002 | LegacyRoutesReturnNotFoundTest.php | Legacy detail URLs return 404 |
|
||||
| T-078-004 | LegacyRoutesReturnNotFoundTest.php | Route names not registered |
|
||||
| T-078-005 | CanonicalDetailRenderTest.php | No "Admin details" link |
|
||||
| T-078-005 | RelatedLinksOnDetailTest.php | No "Admin details" link |
|
||||
| T-078-006 | KpiHeaderTenantlessTest.php | KPI hidden without tenant |
|
||||
| T-078-008 | VerificationReportTenantlessTest.php | Verification report renders tenantless |
|
||||
| T-078-009 | TenantListRedirectTest.php | List redirect 302 |
|
||||
| T-078-010 | RelatedLinksOnDetailTest.php | Related links in header actions |
|
||||
| T-078-011 | OperationsListTenantlessSafetyTest.php | List renders safely with tenant and tenantless context |
|
||||
| T-078-012 | RelatedLinksOnDetailTest.php | Canonical CTA label is "View run" and legacy CTA removed |
|
||||
|
||||
### Updated Tests (existing)
|
||||
|
||||
@ -261,8 +265,9 @@ ### Updated Tests (existing)
|
||||
| VerificationAuthorizationTest.php | `getUrl('view')` replaced with `route('admin.operations.view')` |
|
||||
| FailureSanitizationTest.php | `getUrl('view')` replaced with canonical route; ViewOperationRun replaced with TenantlessOperationRunViewer |
|
||||
| CanonicalViewRunLinksTest.php | Update guard regex for headless resource |
|
||||
| VerificationDbOnlyTest.php | ViewOperationRun replaced with TenantlessOperationRunViewer |
|
||||
| VerificationReportRenderingTest.php | ViewOperationRun replaced with TenantlessOperationRunViewer |
|
||||
| VerificationReportViewerDbOnlyTest.php | ViewOperationRun replaced with TenantlessOperationRunViewer |
|
||||
| VerificationReportRedactionTest.php | ViewOperationRun replaced with TenantlessOperationRunViewer |
|
||||
| VerificationReportMissingOrMalformedTest.php | ViewOperationRun replaced with TenantlessOperationRunViewer |
|
||||
| OperationsCanonicalUrlsTest.php | Remove ListOperationRuns test, add route-gone assertion |
|
||||
| OperationsTenantScopeTest.php | Remove ListOperationRuns reference |
|
||||
|
||||
@ -275,8 +280,9 @@ ### Focused Test Command
|
||||
tests/Feature/Monitoring/OperationsCanonicalUrlsTest.php \
|
||||
tests/Feature/Monitoring/OperationsTenantScopeTest.php \
|
||||
tests/Feature/Verification/VerificationAuthorizationTest.php \
|
||||
tests/Feature/Verification/VerificationDbOnlyTest.php \
|
||||
tests/Feature/Verification/VerificationReportRenderingTest.php \
|
||||
tests/Feature/Verification/VerificationReportViewerDbOnlyTest.php \
|
||||
tests/Feature/Verification/VerificationReportRedactionTest.php \
|
||||
tests/Feature/Verification/VerificationReportMissingOrMalformedTest.php \
|
||||
tests/Feature/OpsUx/FailureSanitizationTest.php \
|
||||
tests/Feature/OpsUx/CanonicalViewRunLinksTest.php
|
||||
```
|
||||
|
||||
@ -41,14 +41,16 @@ ### 4. Run tests
|
||||
```bash
|
||||
# Run the focused test pack for this spec:
|
||||
vendor/bin/sail artisan test --compact \
|
||||
tests/Feature/078/ \
|
||||
tests/Feature/Operations/TenantlessOperationRunViewerTest.php \
|
||||
tests/Feature/Monitoring/OperationsCanonicalUrlsTest.php \
|
||||
tests/Feature/Monitoring/OperationsTenantScopeTest.php \
|
||||
tests/Feature/Verification/VerificationAuthorizationTest.php \
|
||||
tests/Feature/OpsUx/FailureSanitizationTest.php \
|
||||
tests/Feature/OpsUx/CanonicalViewRunLinksTest.php \
|
||||
tests/Feature/Verification/VerificationDbOnlyTest.php \
|
||||
tests/Feature/Verification/VerificationReportRenderingTest.php
|
||||
tests/Feature/Verification/VerificationReportViewerDbOnlyTest.php \
|
||||
tests/Feature/Verification/VerificationReportRedactionTest.php \
|
||||
tests/Feature/Verification/VerificationReportMissingOrMalformedTest.php
|
||||
|
||||
# Expected: All pass
|
||||
```
|
||||
|
||||
@ -86,7 +86,7 @@ ## R-004 — KPI Header Tenantless Behavior
|
||||
|
||||
## R-005 — Legacy List Redirect (FR-078-012)
|
||||
|
||||
**Decision**: Optional 302 redirect from `/admin/t/{tenant}/operations` to `/admin/operations`.
|
||||
**Decision**: Add a 302 redirect from `/admin/t/{tenant}/operations` to `/admin/operations`.
|
||||
|
||||
**Rationale**: Unlike detail URLs which naturally 404 after page decommission, the list URL pattern may still be reached through Filament's navigation system during the transition period. A simple redirect avoids confusion.
|
||||
|
||||
|
||||
@ -181,7 +181,7 @@ ### Functional Requirements
|
||||
#### 5.1 Canonical Run Detail — Feature-Complete Tenantless Page
|
||||
|
||||
- **FR-078-001**: `GET /admin/operations/{run}` MUST display: run summary (status/outcome/timestamps/initiator), target scope, verification report (DB-only), summary counts, failure summary, and context JSON (redacted).
|
||||
- **FR-078-002**: Tenantless detail page MUST reuse `OperationRunResource::infolist()` schema by implementing `HasInfolists` + `InteractsWithInfolists` on `TenantlessOperationRunViewer` and delegating to a shared static infolist builder method. Implementation approach: add `use InteractsWithInfolists; implements HasInfolists` to the page, define `public function operationRunInfolist(Schema $schema): Schema` that calls `OperationRunResource::infolist($schema)`, and render via `{{ $this->operationRunInfolist }}` in the Blade view. If the `InteractsWithInfolists` trait proves incompatible with a standalone Filament Page in v5, the fallback approach is extracting the infolist schema components into a shared static builder and rendering via a `ViewEntry` component in the Blade template.
|
||||
- **FR-078-002**: Tenantless detail page MUST reuse `OperationRunResource::infolist()` via Filament v5's native schema system. `TenantlessOperationRunViewer` MUST define `public function infolist(Schema $schema): Schema` delegating to `OperationRunResource::infolist($schema)`, provide `defaultInfolist(Schema $schema)` to bind `->record($this->run)->columns(2)`, and render the schema via `{{ $this->infolist }}` (or `EmbeddedSchema::make('infolist')` in `content()`).
|
||||
- **FR-078-003**: Tenantless detail MUST reuse `OperationRunLinks::related($run, $tenant)` for contextual header actions (replacing the removed "Admin details" button). If `$run->tenant` is null, only generic links (Operations index) appear.
|
||||
|
||||
#### 5.2 Decommission Auto-Generated Tenant-Scoped Pages
|
||||
@ -193,16 +193,16 @@ #### 5.3 Legacy URL Handling
|
||||
|
||||
Legacy detail URLs (`/admin/t/{tenant}/operations/r/{record}` and `/admin/operations/r/{record}`) naturally return 404 after route decommission — no redirect handlers are needed. ~~FR-078-005 and FR-078-006 removed.~~
|
||||
|
||||
- **FR-078-012**: Optionally add a convenience redirect for `GET /admin/t/{tenant}/operations`:
|
||||
- **FR-078-012**: Add a convenience redirect for `GET /admin/t/{tenant}/operations`:
|
||||
1. Redirect **302** to `/admin/operations`.
|
||||
|
||||
Note: No auth check needed since `/admin/operations` already enforces workspace membership. This redirect is a convenience only; the route could also be left to 404 naturally.
|
||||
Note: No auth check needed since `/admin/operations` already enforces workspace membership.
|
||||
|
||||
#### 5.4 KPI Header Handling (Phase 1 Simplification)
|
||||
|
||||
- **FR-078-008**: `OperationsKpiHeader` widget MUST continue to render when tenant context is available. When no tenant context exists (tenantless pages), the KPI header SHOULD be hidden (not rendered) rather than showing zeros. Full workspace-scoped KPI support is deferred to a follow-up spec.
|
||||
|
||||
- **FR-078-009**: Polling/active-run queries on the canonical list page (`Operations.php`) MUST handle `tenant_id = null` runs and tenantless rendering safely. The existing workspace-scoped query in `Operations.php` already handles this; no breaking changes expected.
|
||||
- **FR-078-009**: Polling/active-run queries on the canonical list page (`Operations.php`) MUST handle `tenant_id = null` runs and tenantless rendering safely. This MUST be verified by an explicit regression test covering list rendering with and without tenant context.
|
||||
|
||||
#### 5.5 Dead Code Cleanup
|
||||
|
||||
@ -290,6 +290,14 @@ ### T-078-010 — Related links appear on canonical detail
|
||||
- Create run of type `restore.execute` with `restore_run_id` in context
|
||||
- Assert header actions include "Restore Run" link
|
||||
|
||||
### T-078-011 — Operations list tenantless safety
|
||||
- Visit `/admin/operations` with and without tenant context
|
||||
- Assert list renders successfully and includes `tenant_id = null` runs without polling/query errors
|
||||
|
||||
### T-078-012 — Canonical CTA label consistency
|
||||
- Assert run entry points use the exact label **"View run"**
|
||||
- Assert canonical detail no longer shows the legacy "Admin details" CTA
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
@ -311,11 +319,10 @@ ## Implementation Notes (Non-Normative)
|
||||
|
||||
### Infolist Reuse Strategy
|
||||
|
||||
**Primary approach**: Add `implements HasInfolists` and `use InteractsWithInfolists` to `TenantlessOperationRunViewer`. Define a named infolist method that delegates to `OperationRunResource::infolist()`. Render via `{{ $this->operationRunInfolist }}` in the Blade template, replacing the current hand-crafted HTML.
|
||||
|
||||
**Fallback approach** (if trait is incompatible with standalone Filament Page in v5): Extract the infolist schema into a shared static builder method on `OperationRunResource` and render it through a `ViewEntry` component in the existing Blade template structure.
|
||||
|
||||
Both approaches require prototyping in a session branch before committing to a plan.
|
||||
Use Filament v5's native schema flow on the standalone page:
|
||||
- Define `infolist(Schema $schema)` and delegate to `OperationRunResource::infolist($schema)`
|
||||
- Define `defaultInfolist(Schema $schema)` with `->record($this->run)->columns(2)`
|
||||
- Render with `{{ $this->infolist }}` (and use `EmbeddedSchema::make('infolist')` from `content()` when needed)
|
||||
|
||||
### Resource After Decommission
|
||||
|
||||
|
||||
@ -27,8 +27,8 @@ ## Phase 1: Setup
|
||||
|
||||
**Purpose**: Create test directory and verify branch readiness
|
||||
|
||||
- [ ] T001 Create spec test directory `tests/Feature/078/`
|
||||
- [ ] T002 Verify branch is clean and on `078-operations-tenantless-canonical`
|
||||
- [X] T001 Create spec test directory `tests/Feature/078/`
|
||||
- [X] T002 Verify branch is clean and on `078-operations-tenantless-canonical`
|
||||
|
||||
---
|
||||
|
||||
@ -38,20 +38,20 @@ ## Phase 2: Foundational (Blocking Prerequisites)
|
||||
|
||||
**Why blocking**: US1 needs ViewOperationRun deleted so infolist lives on standalone page. US2 needs routes removed. US3 needs the viewer to be the sole detail surface. All existing tests must pass after this phase.
|
||||
|
||||
- [ ] T003 Change `getPages()` to return `[]` in `app/Filament/Resources/OperationRunResource.php`
|
||||
- [ ] T004 [P] Delete `app/Filament/Resources/OperationRunResource/Pages/ViewOperationRun.php`
|
||||
- [ ] T005 [P] Delete `app/Filament/Resources/OperationRunResource/Pages/ListOperationRuns.php`
|
||||
- [ ] T006 [P] Delete `app/Livewire/Monitoring/OperationsDetail.php` (dead code)
|
||||
- [ ] T007 [P] Delete `resources/views/livewire/monitoring/operations-detail.blade.php` (dead code)
|
||||
- [ ] T008 Update `tests/Feature/Verification/VerificationAuthorizationTest.php` — replace `OperationRunResource::getUrl('view', ...)` with `route('admin.operations.view', ...)` and replace `ViewOperationRun` Livewire mount with `TenantlessOperationRunViewer`
|
||||
- [ ] T009 [P] Update `tests/Feature/OpsUx/FailureSanitizationTest.php` — replace `OperationRunResource::getUrl('view', ...)` with canonical route; replace `ViewOperationRun` mount with `TenantlessOperationRunViewer`
|
||||
- [ ] T010 [P] Update `tests/Feature/OpsUx/CanonicalViewRunLinksTest.php` — update guard regex to account for headless resource (no `getUrl` calls exist)
|
||||
- [ ] T011 [P] Update `tests/Feature/Verification/VerificationDbOnlyTest.php` — replace `ViewOperationRun` mount with `TenantlessOperationRunViewer`
|
||||
- [ ] T012 [P] Update `tests/Feature/Verification/VerificationReportRenderingTest.php` — replace `ViewOperationRun` mount with `TenantlessOperationRunViewer`
|
||||
- [ ] T013 [P] Update `tests/Feature/Monitoring/OperationsCanonicalUrlsTest.php` — remove `ListOperationRuns` test block; add route-not-registered assertion for `filament.admin.resources.operations.index`
|
||||
- [ ] T014 [P] Update `tests/Feature/Monitoring/OperationsTenantScopeTest.php` — remove `ListOperationRuns` reference
|
||||
- [ ] T015 Run `grep -r "ViewOperationRun\|ListOperationRuns" app/ tests/ resources/` to verify no stale references remain
|
||||
- [ ] T016 Run existing test suite for affected files: `vendor/bin/sail artisan test --compact tests/Feature/Verification/ tests/Feature/OpsUx/ tests/Feature/Monitoring/ tests/Feature/Operations/`
|
||||
- [X] T003 Change `getPages()` to return `[]` in `app/Filament/Resources/OperationRunResource.php`
|
||||
- [X] T004 [P] Delete `app/Filament/Resources/OperationRunResource/Pages/ViewOperationRun.php`
|
||||
- [X] T005 [P] Delete `app/Filament/Resources/OperationRunResource/Pages/ListOperationRuns.php`
|
||||
- [X] T006 [P] Delete `app/Livewire/Monitoring/OperationsDetail.php` (dead code)
|
||||
- [X] T007 [P] Delete `resources/views/livewire/monitoring/operations-detail.blade.php` (dead code)
|
||||
- [X] T008 Update `tests/Feature/Verification/VerificationAuthorizationTest.php` — replace `OperationRunResource::getUrl('view', ...)` with `route('admin.operations.view', ...)` and replace `ViewOperationRun` Livewire mount with `TenantlessOperationRunViewer`
|
||||
- [X] T009 [P] Update `tests/Feature/OpsUx/FailureSanitizationTest.php` — replace `OperationRunResource::getUrl('view', ...)` with canonical route; replace `ViewOperationRun` mount with `TenantlessOperationRunViewer`
|
||||
- [X] T010 [P] Update `tests/Feature/OpsUx/CanonicalViewRunLinksTest.php` — update guard regex to account for headless resource (no `getUrl` calls exist)
|
||||
- [X] T011 [P] Update `tests/Feature/Verification/VerificationReportViewerDbOnlyTest.php` — replace `ViewOperationRun` mount with `TenantlessOperationRunViewer`
|
||||
- [X] T012 [P] Update `tests/Feature/Verification/VerificationReportRedactionTest.php` + `tests/Feature/Verification/VerificationReportMissingOrMalformedTest.php` — replace `ViewOperationRun` mounts with `TenantlessOperationRunViewer`
|
||||
- [X] T013 [P] Update `tests/Feature/Monitoring/OperationsCanonicalUrlsTest.php` — remove `ListOperationRuns` test block; add route-not-registered assertion for `filament.admin.resources.operations.index`
|
||||
- [X] T014 [P] Update `tests/Feature/Monitoring/OperationsTenantScopeTest.php` — remove `ListOperationRuns` reference
|
||||
- [X] T015 Run `grep -r "ViewOperationRun\|ListOperationRuns" app/ tests/ resources/` to verify no stale references remain
|
||||
- [X] T016 Run existing test suite for affected files: `vendor/bin/sail artisan test --compact tests/Feature/Verification/ tests/Feature/OpsUx/ tests/Feature/Monitoring/ tests/Feature/Operations/`
|
||||
|
||||
**Checkpoint**: All existing tests pass. Resource is headless. Dead code removed. No stale references.
|
||||
|
||||
@ -65,20 +65,20 @@ ## Phase 3: User Story 1 — View operation run via canonical URL (Priority: P1)
|
||||
|
||||
### Tests for User Story 1
|
||||
|
||||
- [ ] T017 [P] [US1] Write test T-078-001 (canonical detail renders with tenant_id) in `tests/Feature/078/CanonicalDetailRenderTest.php` — create OperationRun with tenant_id, visit canonical URL as workspace member, assert 200 + infolist sections visible (status badge, outcome, timestamps, target scope, summary counts)
|
||||
- [ ] T018 [P] [US1] Write test T-078-001 (canonical detail renders without tenant_id) in `tests/Feature/078/CanonicalDetailRenderTest.php` — create OperationRun with tenant_id=null, visit canonical URL, assert 200 + graceful rendering ("No target scope details")
|
||||
- [ ] T019 [P] [US1] Write test T-078-001 (non-member gets 404) in `tests/Feature/078/CanonicalDetailRenderTest.php` — visit as non-member, assert 404
|
||||
- [ ] T020 [P] [US1] Write test T-078-008 (verification report renders tenantless) in `tests/Feature/078/VerificationReportTenantlessTest.php` — create run with verification_report in context, visit canonical URL, assert verification section renders with badges and acknowledgements
|
||||
- [ ] T021 [P] [US1] Write test T-078-007 (DB-only rendering) in `tests/Feature/078/CanonicalDetailRenderTest.php` — assert canonical detail rendering does not dispatch jobs or HTTP calls
|
||||
- [X] T017 [P] [US1] Write test T-078-001 (canonical detail renders with tenant_id) in `tests/Feature/078/CanonicalDetailRenderTest.php` — create OperationRun with tenant_id, visit canonical URL as workspace member, assert 200 + infolist sections visible (status badge, outcome, timestamps, target scope, summary counts)
|
||||
- [X] T018 [P] [US1] Write test T-078-001 (canonical detail renders without tenant_id) in `tests/Feature/078/CanonicalDetailRenderTest.php` — create OperationRun with tenant_id=null, visit canonical URL, assert 200 + graceful rendering ("No target scope details")
|
||||
- [X] T019 [P] [US1] Write test T-078-001 (non-member gets 404) in `tests/Feature/078/CanonicalDetailRenderTest.php` — visit as non-member, assert 404
|
||||
- [X] T020 [P] [US1] Write test T-078-008 (verification report renders tenantless) in `tests/Feature/078/VerificationReportTenantlessTest.php` — create run with verification_report in context, visit canonical URL, assert verification section renders with badges and acknowledgements
|
||||
- [X] T021 [P] [US1] Write test T-078-007 (DB-only rendering) in `tests/Feature/078/CanonicalDetailRenderTest.php` — assert canonical detail rendering does not dispatch jobs or HTTP calls
|
||||
|
||||
### Implementation for User Story 1
|
||||
|
||||
- [ ] T022 [US1] Add `public function infolist(Schema $schema): Schema` to `app/Filament/Pages/Operations/TenantlessOperationRunViewer.php` — delegates to `OperationRunResource::infolist($schema)`
|
||||
- [ ] T023 [US1] Add `public function defaultInfolist(Schema $schema): Schema` to `app/Filament/Pages/Operations/TenantlessOperationRunViewer.php` — sets `->record($this->run)->columns(2)`
|
||||
- [ ] T024 [US1] Add `public bool $opsUxIsTabHidden = false` property to `app/Filament/Pages/Operations/TenantlessOperationRunViewer.php` (required for polling callback in infolist)
|
||||
- [ ] T025 [US1] Add content method or `EmbeddedSchema::make('infolist')` rendering to `app/Filament/Pages/Operations/TenantlessOperationRunViewer.php`
|
||||
- [ ] T026 [US1] Replace hand-coded HTML in `resources/views/filament/pages/operations/tenantless-operation-run-viewer.blade.php` with infolist render (`{{ $this->infolist }}`)
|
||||
- [ ] T027 [US1] Run tests: `vendor/bin/sail artisan test --compact tests/Feature/078/CanonicalDetailRenderTest.php tests/Feature/078/VerificationReportTenantlessTest.php tests/Feature/Operations/TenantlessOperationRunViewerTest.php`
|
||||
- [X] T022 [US1] Add `public function infolist(Schema $schema): Schema` to `app/Filament/Pages/Operations/TenantlessOperationRunViewer.php` — delegates to `OperationRunResource::infolist($schema)`
|
||||
- [X] T023 [US1] Add `public function defaultInfolist(Schema $schema): Schema` to `app/Filament/Pages/Operations/TenantlessOperationRunViewer.php` — sets `->record($this->run)->columns(2)`
|
||||
- [X] T024 [US1] Add `public bool $opsUxIsTabHidden = false` property to `app/Filament/Pages/Operations/TenantlessOperationRunViewer.php` (required for polling callback in infolist)
|
||||
- [X] T025 [US1] Add `public function content(Schema $schema): Schema` to `app/Filament/Pages/Operations/TenantlessOperationRunViewer.php` returning `EmbeddedSchema::make('infolist')`
|
||||
- [X] T026 [US1] Replace hand-coded HTML in `resources/views/filament/pages/operations/tenantless-operation-run-viewer.blade.php` with infolist render (`{{ $this->infolist }}`)
|
||||
- [X] T027 [US1] Run tests: `vendor/bin/sail artisan test --compact tests/Feature/078/CanonicalDetailRenderTest.php tests/Feature/078/VerificationReportTenantlessTest.php tests/Feature/Operations/TenantlessOperationRunViewerTest.php`
|
||||
|
||||
**Checkpoint**: Canonical detail at `/admin/operations/{run}` renders full Filament infolist. Runs with and without tenant_id render correctly. MVP is functional.
|
||||
|
||||
@ -94,15 +94,15 @@ ## Phase 4: User Story 2 — Legacy tenant-scoped detail URLs return 404 (Priori
|
||||
|
||||
### Tests for User Story 2
|
||||
|
||||
- [ ] T028 [P] [US2] Write test T-078-002 (legacy detail URL returns 404) in `tests/Feature/078/LegacyRoutesReturnNotFoundTest.php` — visit `/admin/t/{tenant}/operations/r/{record}` as any user, assert 404
|
||||
- [ ] T029 [P] [US2] Write test T-078-002 (slug variant returns 404) in `tests/Feature/078/LegacyRoutesReturnNotFoundTest.php` — visit `/admin/operations/r/{record}`, assert 404
|
||||
- [ ] T030 [P] [US2] Write test T-078-004 (auto-generated route names not registered) in `tests/Feature/078/LegacyRoutesReturnNotFoundTest.php` — assert `Route::has('filament.admin.resources.operations.view')` is false and `Route::has('filament.admin.resources.operations.index')` is false
|
||||
- [X] T028 [P] [US2] Write test T-078-002 (legacy detail URL returns 404) in `tests/Feature/078/LegacyRoutesReturnNotFoundTest.php` — visit `/admin/t/{tenant}/operations/r/{record}` as any user, assert 404
|
||||
- [X] T029 [P] [US2] Write test T-078-002 (slug variant returns 404) in `tests/Feature/078/LegacyRoutesReturnNotFoundTest.php` — visit `/admin/operations/r/{record}`, assert 404
|
||||
- [X] T030 [P] [US2] Write test T-078-004 (auto-generated route names not registered) in `tests/Feature/078/LegacyRoutesReturnNotFoundTest.php` — assert `Route::has('filament.admin.resources.operations.view')` is false and `Route::has('filament.admin.resources.operations.index')` is false
|
||||
|
||||
### Implementation for User Story 2
|
||||
|
||||
> No additional implementation needed — routes were removed in Phase 2.
|
||||
|
||||
- [ ] T031 [US2] Run tests: `vendor/bin/sail artisan test --compact tests/Feature/078/LegacyRoutesReturnNotFoundTest.php`
|
||||
- [X] T031 [US2] Run tests: `vendor/bin/sail artisan test --compact tests/Feature/078/LegacyRoutesReturnNotFoundTest.php`
|
||||
|
||||
**Checkpoint**: All legacy URLs return 404. Route names are not registered. No existence leakage.
|
||||
|
||||
@ -116,15 +116,15 @@ ## Phase 5: User Story 3 — Contextual navigation from run detail (Priority: P2
|
||||
|
||||
### Tests for User Story 3
|
||||
|
||||
- [ ] T032 [P] [US3] Write test T-078-010 (related links appear) in `tests/Feature/078/RelatedLinksOnDetailTest.php` — create run of type `restore.execute` with `restore_run_id` in context, visit canonical detail, assert header actions include "Restore Run" link
|
||||
- [ ] T033 [P] [US3] Write test T-078-010 (generic links for tenantless run) in `tests/Feature/078/RelatedLinksOnDetailTest.php` — create run with tenant_id=null, assert only generic links (Operations index) appear
|
||||
- [ ] T034 [P] [US3] Write test T-078-005 (no "Admin details" link) in `tests/Feature/078/RelatedLinksOnDetailTest.php` — assert canonical detail does not render `/admin/t/.../operations/r/...` links
|
||||
- [X] T032 [P] [US3] Write test T-078-010 (related links appear) in `tests/Feature/078/RelatedLinksOnDetailTest.php` — create run of type `restore.execute` with `restore_run_id` in context, visit canonical detail, assert header actions include "Restore Run" link
|
||||
- [X] T033 [P] [US3] Write test T-078-010 (generic links for tenantless run) in `tests/Feature/078/RelatedLinksOnDetailTest.php` — create run with tenant_id=null, assert only generic links (Operations index) appear
|
||||
- [X] T034 [P] [US3] Write tests T-078-005 + T-078-012 in `tests/Feature/078/RelatedLinksOnDetailTest.php` — assert canonical detail does not render `/admin/t/.../operations/r/...` links and uses exact CTA label "View run" (legacy "Admin details" absent)
|
||||
|
||||
### Implementation for User Story 3
|
||||
|
||||
- [ ] T035 [US3] Add `getHeaderActions()` method to `app/Filament/Pages/Operations/TenantlessOperationRunViewer.php` — return actions from `OperationRunLinks::related($this->run, $this->run->tenant)`
|
||||
- [ ] T036 [US3] Remove "Admin details" button code (~line 61) from `app/Filament/Pages/Operations/TenantlessOperationRunViewer.php`
|
||||
- [ ] T037 [US3] Run tests: `vendor/bin/sail artisan test --compact tests/Feature/078/RelatedLinksOnDetailTest.php`
|
||||
- [X] T035 [US3] Add `getHeaderActions()` method to `app/Filament/Pages/Operations/TenantlessOperationRunViewer.php` — return actions from `OperationRunLinks::related($this->run, $this->run->tenant)`
|
||||
- [X] T036 [US3] Remove "Admin details" button code (~line 61) from `app/Filament/Pages/Operations/TenantlessOperationRunViewer.php`
|
||||
- [X] T037 [US3] Run tests: `vendor/bin/sail artisan test --compact tests/Feature/078/RelatedLinksOnDetailTest.php`
|
||||
|
||||
**Checkpoint**: Header shows contextual "Open" action group. "Admin details" link is gone. Related links vary by run type.
|
||||
|
||||
@ -132,20 +132,21 @@ ### Implementation for User Story 3
|
||||
|
||||
## Phase 6: User Story 4 — Operations list remains workspace-scoped (Priority: P3)
|
||||
|
||||
**Goal**: Ensure no regression on `/admin/operations` list. KPI header hidden in tenantless mode. Optional 302 redirect for decommissioned list URL.
|
||||
**Goal**: Ensure no regression on `/admin/operations` list. KPI header hidden in tenantless mode. 302 redirect for decommissioned list URL.
|
||||
|
||||
**Independent Test**: Visit `/admin/operations` with and without tenant context; verify workspace scoping, KPI behavior, and redirect.
|
||||
|
||||
### Tests for User Story 4
|
||||
|
||||
- [ ] T038 [P] [US4] Write test T-078-006 (KPI header hidden without tenant) in `tests/Feature/078/KpiHeaderTenantlessTest.php` — visit `/admin/operations` without tenant context, assert KPI stats are not rendered (empty array from `getStats()`)
|
||||
- [ ] T039 [P] [US4] Write test T-078-009 (tenant-scoped list redirect) in `tests/Feature/078/TenantListRedirectTest.php` — visit `/admin/t/{tenant}/operations`, assert 302 redirect to `/admin/operations`
|
||||
- [X] T038 [P] [US4] Write test T-078-006 (KPI header hidden without tenant) in `tests/Feature/078/KpiHeaderTenantlessTest.php` — visit `/admin/operations` without tenant context, assert KPI stats are not rendered (empty array from `getStats()`)
|
||||
- [X] T039 [P] [US4] Write test T-078-009 (tenant-scoped list redirect) in `tests/Feature/078/TenantListRedirectTest.php` — visit `/admin/t/{tenant}/operations`, assert 302 redirect to `/admin/operations`
|
||||
- [X] T048 [P] [US4] Write test T-078-011 (tenantless list query safety) in `tests/Feature/078/OperationsListTenantlessSafetyTest.php` — visit `/admin/operations` with and without tenant context, assert runs including `tenant_id = null` render without errors
|
||||
|
||||
### Implementation for User Story 4
|
||||
|
||||
- [ ] T040 [US4] Add tenant-null guard in `getStats()` of `app/Filament/Widgets/Operations/OperationsKpiHeader.php` — if `Filament::getTenant()` is null, return `[]`
|
||||
- [ ] T041 [US4] Add 302 redirect route in `routes/web.php` — `/admin/t/{tenant}/operations` redirects to `/admin/operations` (FR-078-012)
|
||||
- [ ] T042 [US4] Run tests: `vendor/bin/sail artisan test --compact tests/Feature/078/KpiHeaderTenantlessTest.php tests/Feature/078/TenantListRedirectTest.php`
|
||||
- [X] T040 [US4] Add tenant-null guard in `getStats()` of `app/Filament/Widgets/Operations/OperationsKpiHeader.php` — if `Filament::getTenant()` is null, return `[]`
|
||||
- [X] T041 [US4] Add 302 redirect route in `routes/web.php` — `/admin/t/{tenant}/operations` redirects to `/admin/operations` (FR-078-012)
|
||||
- [X] T042 [US4] Run tests: `vendor/bin/sail artisan test --compact tests/Feature/078/KpiHeaderTenantlessTest.php tests/Feature/078/TenantListRedirectTest.php tests/Feature/078/OperationsListTenantlessSafetyTest.php`
|
||||
|
||||
**Checkpoint**: Operations list works in workspace mode. KPI hidden without tenant. List redirect works.
|
||||
|
||||
@ -155,11 +156,12 @@ ## Phase 7: Polish & Cross-Cutting Concerns
|
||||
|
||||
**Purpose**: Final validation, formatting, and stale reference sweep
|
||||
|
||||
- [ ] T043 Run `grep -r "ViewOperationRun\|ListOperationRuns\|OperationsDetail" app/ tests/ resources/` — verify zero stale references across entire codebase
|
||||
- [ ] T044 Run `vendor/bin/sail bin pint --dirty` — fix any formatting issues
|
||||
- [ ] T045 Run focused test pack: `vendor/bin/sail artisan test --compact tests/Feature/078/ tests/Feature/Operations/TenantlessOperationRunViewerTest.php tests/Feature/Monitoring/OperationsCanonicalUrlsTest.php tests/Feature/Monitoring/OperationsTenantScopeTest.php tests/Feature/Verification/VerificationAuthorizationTest.php tests/Feature/Verification/VerificationDbOnlyTest.php tests/Feature/Verification/VerificationReportRenderingTest.php tests/Feature/OpsUx/FailureSanitizationTest.php tests/Feature/OpsUx/CanonicalViewRunLinksTest.php`
|
||||
- [ ] T046 Run quickstart.md validation: `vendor/bin/sail artisan route:list --name=filament.admin.resources.operations` — assert empty output
|
||||
- [ ] T047 Ask user if they want to run the full test suite: `vendor/bin/sail artisan test --compact`
|
||||
- [X] T043 Run `grep -r "ViewOperationRun\|ListOperationRuns\|OperationsDetail" app/ tests/ resources/` — verify zero stale references across entire codebase
|
||||
- [X] T044 Run `vendor/bin/sail bin pint --dirty` — fix any formatting issues
|
||||
- [X] T049 Remove obsolete temporary layout `resources/views/filament/layouts/topbar-only.blade.php`
|
||||
- [X] T045 Run focused test pack: `vendor/bin/sail artisan test --compact tests/Feature/078/ tests/Feature/Operations/TenantlessOperationRunViewerTest.php tests/Feature/Monitoring/OperationsCanonicalUrlsTest.php tests/Feature/Monitoring/OperationsTenantScopeTest.php tests/Feature/Verification/VerificationAuthorizationTest.php tests/Feature/Verification/VerificationReportViewerDbOnlyTest.php tests/Feature/Verification/VerificationReportRedactionTest.php tests/Feature/Verification/VerificationReportMissingOrMalformedTest.php tests/Feature/OpsUx/FailureSanitizationTest.php tests/Feature/OpsUx/CanonicalViewRunLinksTest.php`
|
||||
- [X] T046 Run quickstart.md validation: `vendor/bin/sail artisan route:list --name=filament.admin.resources.operations` — assert empty output
|
||||
- [X] T047 Ask user if they want to run the full test suite: `vendor/bin/sail artisan test --compact`
|
||||
|
||||
---
|
||||
|
||||
|
||||
129
tests/Feature/078/CanonicalDetailRenderTest.php
Normal file
129
tests/Feature/078/CanonicalDetailRenderTest.php
Normal file
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\Tenant;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class CanonicalDetailRenderTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_renders_canonical_detail_for_a_workspace_member_when_tenant_context_exists(): void
|
||||
{
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
Filament::setTenant(null, true);
|
||||
|
||||
$run = OperationRun::factory()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'type' => 'policy.sync',
|
||||
'status' => 'completed',
|
||||
'outcome' => 'succeeded',
|
||||
'context' => [
|
||||
'target_scope' => [
|
||||
'entra_tenant_name' => 'Contoso',
|
||||
'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
|
||||
],
|
||||
],
|
||||
'summary_counts' => [
|
||||
'total' => 10,
|
||||
'processed' => 10,
|
||||
'succeeded' => 10,
|
||||
'failed' => 0,
|
||||
'skipped' => 0,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
|
||||
->assertOk()
|
||||
->assertSee('Operation run')
|
||||
->assertSee('Policy sync')
|
||||
->assertSee('Counts')
|
||||
->assertSee('Context')
|
||||
->assertSee('Contoso');
|
||||
}
|
||||
|
||||
public function test_renders_canonical_detail_gracefully_when_tenant_id_is_null(): void
|
||||
{
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
Filament::setTenant(null, true);
|
||||
|
||||
$run = OperationRun::factory()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'tenant_id' => null,
|
||||
'type' => 'provider.connection.check',
|
||||
'status' => 'completed',
|
||||
'outcome' => 'failed',
|
||||
'context' => [],
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
|
||||
->assertOk()
|
||||
->assertSee('No target scope details were recorded for this run.');
|
||||
}
|
||||
|
||||
public function test_returns_404_on_canonical_detail_for_non_members(): void
|
||||
{
|
||||
$tenant = Tenant::factory()->create();
|
||||
[$otherUser] = createUserWithTenant(role: 'owner');
|
||||
|
||||
$run = OperationRun::factory()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'type' => 'policy.sync',
|
||||
'status' => 'completed',
|
||||
'outcome' => 'succeeded',
|
||||
]);
|
||||
|
||||
$this->actingAs($otherUser)
|
||||
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
|
||||
->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_renders_canonical_detail_db_only_with_no_job_dispatch(): void
|
||||
{
|
||||
Bus::fake();
|
||||
Queue::fake();
|
||||
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
$run = OperationRun::factory()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'type' => 'provider.connection.check',
|
||||
'status' => 'completed',
|
||||
'outcome' => 'failed',
|
||||
'context' => [
|
||||
'verification_report' => json_decode(
|
||||
(string) file_get_contents(base_path('specs/074-verification-checklist/contracts/examples/fail.json')),
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR,
|
||||
),
|
||||
],
|
||||
]);
|
||||
|
||||
assertNoOutboundHttp(function () use ($user, $run): void {
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
|
||||
->assertOk()
|
||||
->assertSee('Verification report');
|
||||
});
|
||||
|
||||
Bus::assertNothingDispatched();
|
||||
Queue::assertNothingPushed();
|
||||
}
|
||||
}
|
||||
34
tests/Feature/078/KpiHeaderTenantlessTest.php
Normal file
34
tests/Feature/078/KpiHeaderTenantlessTest.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\OperationRun;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class KpiHeaderTenantlessTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_hides_operations_kpi_stats_when_tenant_context_is_absent(): void
|
||||
{
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
OperationRun::factory()->count(3)->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
]);
|
||||
|
||||
Filament::setTenant(null, true);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get(route('admin.operations.index'))
|
||||
->assertOk()
|
||||
->assertDontSee('Total Runs (30 days)')
|
||||
->assertDontSee('Failed/Partial (7 days)')
|
||||
->assertDontSee('Avg Duration (7 days)');
|
||||
}
|
||||
}
|
||||
42
tests/Feature/078/LegacyRoutesReturnNotFoundTest.php
Normal file
42
tests/Feature/078/LegacyRoutesReturnNotFoundTest.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\OperationRun;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class LegacyRoutesReturnNotFoundTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_returns_404_for_legacy_tenant_scoped_operation_detail_urls(): void
|
||||
{
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
$run = OperationRun::factory()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get('/admin/t/'.$tenant->external_id.'/operations/r/'.$run->getKey())
|
||||
->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_returns_404_for_the_admin_operations_r_record_legacy_slug_variant(): void
|
||||
{
|
||||
[$user] = createUserWithTenant(role: 'owner');
|
||||
|
||||
$this->actingAs($user)
|
||||
->get('/admin/operations/r/123')
|
||||
->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_does_not_register_legacy_operation_resource_route_names(): void
|
||||
{
|
||||
$this->assertFalse(Route::has('filament.admin.resources.operations.view'));
|
||||
$this->assertFalse(Route::has('filament.admin.resources.operations.index'));
|
||||
}
|
||||
}
|
||||
78
tests/Feature/078/OperationsListTenantlessSafetyTest.php
Normal file
78
tests/Feature/078/OperationsListTenantlessSafetyTest.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\OperationRun;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class OperationsListTenantlessSafetyTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_renders_workspace_operations_list_with_tenantless_runs_when_no_tenant_context_is_set(): void
|
||||
{
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
OperationRun::factory()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'tenant_id' => null,
|
||||
'type' => 'provider.connection.check',
|
||||
'initiator_name' => 'Tenantless run',
|
||||
'status' => 'queued',
|
||||
'outcome' => 'pending',
|
||||
]);
|
||||
|
||||
OperationRun::factory()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'type' => 'policy.sync',
|
||||
'initiator_name' => 'Tenant run',
|
||||
'status' => 'queued',
|
||||
'outcome' => 'pending',
|
||||
]);
|
||||
|
||||
Filament::setTenant(null, true);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get(route('admin.operations.index'))
|
||||
->assertOk()
|
||||
->assertSee('Tenantless run')
|
||||
->assertSee('Tenant run');
|
||||
}
|
||||
|
||||
public function test_renders_workspace_operations_list_safely_with_tenant_context_and_tenantless_records_present(): void
|
||||
{
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
OperationRun::factory()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'tenant_id' => null,
|
||||
'type' => 'provider.connection.check',
|
||||
'initiator_name' => 'Tenantless run',
|
||||
'status' => 'queued',
|
||||
'outcome' => 'pending',
|
||||
]);
|
||||
|
||||
OperationRun::factory()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'type' => 'policy.sync',
|
||||
'initiator_name' => 'Tenant run',
|
||||
'status' => 'queued',
|
||||
'outcome' => 'pending',
|
||||
]);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get(route('admin.operations.index'))
|
||||
->assertOk()
|
||||
->assertSee('Tenant run')
|
||||
->assertDontSee('Tenantless run');
|
||||
}
|
||||
}
|
||||
94
tests/Feature/078/RelatedLinksOnDetailTest.php
Normal file
94
tests/Feature/078/RelatedLinksOnDetailTest.php
Normal file
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\RestoreRun;
|
||||
use App\Support\OperationRunLinks;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class RelatedLinksOnDetailTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_shows_restore_related_links_on_canonical_detail_for_restore_execute_runs(): void
|
||||
{
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
$restoreRun = RestoreRun::factory()->create([
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
]);
|
||||
|
||||
$run = OperationRun::factory()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'type' => 'restore.execute',
|
||||
'context' => [
|
||||
'restore_run_id' => (int) $restoreRun->getKey(),
|
||||
],
|
||||
]);
|
||||
|
||||
$expectedUrl = OperationRunLinks::related($run->loadMissing('tenant'), $tenant)['Restore Run'] ?? null;
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
|
||||
->assertOk()
|
||||
->assertSee('Open')
|
||||
->assertSee('Restore Run');
|
||||
|
||||
$this->assertIsString($expectedUrl);
|
||||
$response->assertSee((string) $expectedUrl, false);
|
||||
}
|
||||
|
||||
public function test_shows_only_generic_links_for_tenantless_runs_on_canonical_detail(): void
|
||||
{
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
$run = OperationRun::factory()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'tenant_id' => null,
|
||||
'type' => 'restore.execute',
|
||||
'context' => [
|
||||
'restore_run_id' => 999,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
|
||||
->assertOk()
|
||||
->assertSee('Operations')
|
||||
->assertSee(route('admin.operations.index'), false)
|
||||
->assertDontSee('Restore Run');
|
||||
}
|
||||
|
||||
public function test_does_not_show_legacy_admin_details_cta_and_keeps_canonical_view_run_label(): void
|
||||
{
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
$run = OperationRun::factory()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'type' => 'policy.sync',
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
|
||||
->assertOk()
|
||||
->assertDontSee('Admin details')
|
||||
->assertDontSee('/admin/t/'.$tenant->external_id.'/operations/r/'.$run->getKey(), false);
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get(route('admin.operations.index'))
|
||||
->assertOk()
|
||||
->assertSee('View run');
|
||||
}
|
||||
}
|
||||
26
tests/Feature/078/TenantListRedirectTest.php
Normal file
26
tests/Feature/078/TenantListRedirectTest.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class TenantListRedirectTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_redirects_legacy_tenant_scoped_operations_list_url_to_canonical_operations_index(): void
|
||||
{
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get('/admin/t/'.$tenant->external_id.'/operations')
|
||||
->assertStatus(302)
|
||||
->assertRedirect(route('admin.operations.index'));
|
||||
}
|
||||
}
|
||||
63
tests/Feature/078/VerificationReportTenantlessTest.php
Normal file
63
tests/Feature/078/VerificationReportTenantlessTest.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\OperationRun;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class VerificationReportTenantlessTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_renders_verification_report_on_canonical_detail_without_filament_tenant_context(): void
|
||||
{
|
||||
[$user, $tenant] = createUserWithTenant(role: 'operator');
|
||||
|
||||
$report = json_decode(
|
||||
(string) file_get_contents(base_path('specs/074-verification-checklist/contracts/examples/fail.json')),
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR,
|
||||
);
|
||||
|
||||
$previousRun = OperationRun::factory()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'user_id' => (int) $user->getKey(),
|
||||
'type' => 'provider.connection.check',
|
||||
'status' => 'completed',
|
||||
'outcome' => 'failed',
|
||||
'context' => [
|
||||
'verification_report' => $report,
|
||||
],
|
||||
]);
|
||||
|
||||
$report['previous_report_id'] = (int) $previousRun->getKey();
|
||||
|
||||
$run = OperationRun::factory()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'user_id' => (int) $user->getKey(),
|
||||
'type' => 'provider.connection.check',
|
||||
'status' => 'completed',
|
||||
'outcome' => 'failed',
|
||||
'context' => [
|
||||
'verification_report' => $report,
|
||||
],
|
||||
]);
|
||||
|
||||
Filament::setTenant(null, true);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
|
||||
->assertOk()
|
||||
->assertSee('Verification report')
|
||||
->assertSee('Open previous verification')
|
||||
->assertSee('/admin/operations/'.((int) $previousRun->getKey()), false)
|
||||
->assertSee('Token acquisition works');
|
||||
}
|
||||
}
|
||||
@ -2,12 +2,13 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Resources\OperationRunResource\Pages\ListOperationRuns;
|
||||
use App\Filament\Pages\Monitoring\Operations;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\Tenant;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
@ -68,7 +69,7 @@
|
||||
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
|
||||
->assertOk()
|
||||
->assertSee('Operation run')
|
||||
->assertSee('/admin/t/'.((int) $tenant->getKey()).'/operations/r/'.((int) $run->getKey()));
|
||||
->assertDontSee('/admin/t/'.((int) $tenant->getKey()).'/operations/r/'.((int) $run->getKey()));
|
||||
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
@ -116,7 +117,7 @@
|
||||
]);
|
||||
|
||||
$component = Livewire::actingAs($user)
|
||||
->test(ListOperationRuns::class)
|
||||
->test(Operations::class)
|
||||
->assertCanSeeTableRecords([$runA])
|
||||
->assertCanNotSeeTableRecords([$runB]);
|
||||
|
||||
@ -125,6 +126,11 @@
|
||||
->assertCanSeeTableRecords([$runA, $runB]);
|
||||
});
|
||||
|
||||
it('does not register legacy operation resource routes', function (): void {
|
||||
expect(Route::has('filament.admin.resources.operations.index'))->toBeFalse();
|
||||
expect(Route::has('filament.admin.resources.operations.view'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('has reserved Monitoring placeholder pages for Alerts and Audit Log', function (): void {
|
||||
$tenant = Tenant::factory()->create();
|
||||
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner');
|
||||
|
||||
@ -26,10 +26,10 @@
|
||||
$this->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get('/admin/operations')
|
||||
->assertOk()
|
||||
->assertSee('Total Runs (30 days)')
|
||||
->assertSee('Active Runs')
|
||||
->assertSee('Failed/Partial (7 days)')
|
||||
->assertSee('Avg Duration (7 days)')
|
||||
->assertDontSee('Total Runs (30 days)')
|
||||
->assertDontSee('Active Runs')
|
||||
->assertDontSee('Failed/Partial (7 days)')
|
||||
->assertDontSee('Avg Duration (7 days)')
|
||||
->assertSee('All')
|
||||
->assertSee('Active')
|
||||
->assertSee('Succeeded')
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Filament\Resources\OperationRunResource\Pages\ListOperationRuns;
|
||||
use App\Filament\Pages\Monitoring\Operations;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\Tenant;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
@ -118,7 +118,7 @@
|
||||
]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(ListOperationRuns::class)
|
||||
->test(Operations::class)
|
||||
->assertCanSeeTableRecords([$runActiveA, $runSucceededA, $runPartialA, $runFailedA])
|
||||
->assertCanNotSeeTableRecords([$runActiveB, $runFailedB])
|
||||
->set('activeTab', 'active')
|
||||
|
||||
@ -22,7 +22,8 @@
|
||||
|
||||
$contents = File::get($path);
|
||||
|
||||
if (preg_match("/\\bOperationRunResource::getUrl\(\\s*'view'/", $contents) === 1) {
|
||||
if (preg_match("/\\bOperationRunResource::getUrl\(\\s*'view'/", $contents) === 1
|
||||
|| preg_match("/route\(\s*'filament\.admin\.resources\.operations\.view'/", $contents) === 1) {
|
||||
$violations[] = $path;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
<?php
|
||||
|
||||
use App\Filament\Resources\OperationRunResource;
|
||||
use App\Models\Tenant;
|
||||
use App\Services\OperationRunService;
|
||||
use Illuminate\Notifications\DatabaseNotification;
|
||||
@ -55,6 +54,6 @@
|
||||
expect($notificationJson)->not->toContain('test.user@example.com');
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(OperationRunResource::getUrl('view', ['record' => $run], tenant: $tenant))
|
||||
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
|
||||
->assertSuccessful();
|
||||
});
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Resources\OperationRunResource;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\ProviderConnection;
|
||||
use App\Models\Tenant;
|
||||
@ -35,7 +34,7 @@
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(OperationRunResource::getUrl('view', ['record' => $run], tenant: $tenant))
|
||||
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
|
||||
->assertStatus(404);
|
||||
|
||||
$connection = ProviderConnection::factory()->create([
|
||||
@ -71,7 +70,7 @@
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(OperationRunResource::getUrl('view', ['record' => $run], tenant: $tenant))
|
||||
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
|
||||
->assertOk()
|
||||
->assertSee('Verification report');
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Resources\OperationRunResource\Pages\ViewOperationRun;
|
||||
use App\Filament\Pages\Operations\TenantlessOperationRunViewer;
|
||||
use App\Models\OperationRun;
|
||||
use Filament\Facades\Filament;
|
||||
use Livewire\Livewire;
|
||||
@ -24,7 +24,7 @@
|
||||
]);
|
||||
|
||||
assertNoOutboundHttp(function () use ($run): void {
|
||||
Livewire::test(ViewOperationRun::class, ['record' => $run->getRouteKey()])
|
||||
Livewire::test(TenantlessOperationRunViewer::class, ['run' => $run])
|
||||
->assertSee('Verification report')
|
||||
->assertSee('Verification report unavailable');
|
||||
});
|
||||
@ -52,7 +52,7 @@
|
||||
]);
|
||||
|
||||
assertNoOutboundHttp(function () use ($run): void {
|
||||
Livewire::test(ViewOperationRun::class, ['record' => $run->getRouteKey()])
|
||||
Livewire::test(TenantlessOperationRunViewer::class, ['run' => $run])
|
||||
->assertSee('Verification report')
|
||||
->assertSee('Verification report unavailable');
|
||||
});
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Resources\OperationRunResource\Pages\ViewOperationRun;
|
||||
use App\Filament\Pages\Operations\TenantlessOperationRunViewer;
|
||||
use App\Models\OperationRun;
|
||||
use App\Support\Verification\VerificationReportFingerprint;
|
||||
use Filament\Facades\Filament;
|
||||
@ -54,7 +54,7 @@
|
||||
$fingerprint = VerificationReportFingerprint::forReport($report);
|
||||
|
||||
assertNoOutboundHttp(function () use ($run, $fingerprint): void {
|
||||
Livewire::test(ViewOperationRun::class, ['record' => $run->getRouteKey()])
|
||||
Livewire::test(TenantlessOperationRunViewer::class, ['run' => $run])
|
||||
->assertSee('Verification report')
|
||||
->assertSee('Open previous verification')
|
||||
->assertSee($fingerprint)
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Resources\OperationRunResource\Pages\ViewOperationRun;
|
||||
use App\Filament\Pages\Operations\TenantlessOperationRunViewer;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\ProviderConnection;
|
||||
use App\Models\Tenant;
|
||||
@ -45,7 +45,7 @@
|
||||
]);
|
||||
|
||||
assertNoOutboundHttp(function () use ($run): void {
|
||||
$component = Livewire::test(ViewOperationRun::class, ['record' => $run->getRouteKey()])
|
||||
$component = Livewire::test(TenantlessOperationRunViewer::class, ['run' => $run])
|
||||
->assertSee('Verification report')
|
||||
->assertSee('Blocked')
|
||||
->assertSee('Token acquisition works');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user