TenantAtlas/apps/platform/app/Filament/Pages/BaselineSubjectResolution.php
ahmido 39298f27f2 feat(ui): implement baseline subject resolution ui (#455)
Added `BaselineSubjectResolution` page and supporting logic to visualize missing identities, ambiguous matches, and skipped coverages as defined in Spec 384. Replaces legacy compare warnings with an actionable, deterministic UI surface.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #455
2026-06-16 23:36:38 +00:00

1072 lines
40 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Filament\Pages;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\ProviderResourceBinding;
use App\Models\User;
use App\Models\Workspace;
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
use App\Services\Baselines\BaselineCompareService;
use App\Services\Baselines\BaselineSubjectResolutionQuery;
use App\Services\Resources\ProviderResourceBindingService;
use App\Support\Auth\Capabilities;
use App\Support\Filament\TablePaginationProfiles;
use App\Support\ManagedEnvironmentLinks;
use App\Support\OperationRunLinks;
use App\Support\OperationRunType;
use App\Support\OpsUx\OperationUxPresenter;
use App\Support\OpsUx\OpsUxBrowserEvents;
use App\Support\Rbac\UiEnforcement;
use App\Support\Resources\ResourceIdentity;
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Livewire\Attributes\Locked;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class BaselineSubjectResolution extends Page implements HasTable
{
use InteractsWithTable;
private const array LEGACY_SCOPE_QUERY_KEYS = [
'environment_id',
'tenant',
'tenant_id',
'managed_environment_id',
'environment',
'tenant_scope',
'tableFilters',
];
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-puzzle-piece';
protected static ?string $navigationLabel = 'Baseline Subject Resolution';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Baseline Subject Resolution';
protected static ?string $slug = 'workspaces/{workspace}/environments/{environment}/baseline-subject-resolution';
protected string $view = 'filament.pages.baseline-subject-resolution';
#[Locked]
public ?int $scopedEnvironmentId = null;
public ?int $focusedOperationRunId = null;
/**
* @param array<mixed> $parameters
*/
public static function getUrl(array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null): string
{
$panelId = $panel ?? Filament::getCurrentOrDefaultPanel()?->getId() ?? 'admin';
if ($panelId !== 'admin') {
return parent::getUrl($parameters, $isAbsolute, $panelId, $tenant);
}
$environment = static::resolveAdminUrlEnvironment($parameters, $tenant);
if (! $environment instanceof ManagedEnvironment) {
return url('/admin');
}
$workspace = static::resolveAdminUrlWorkspace($environment, $parameters);
if (! $workspace instanceof Workspace && ! is_string($workspace) && ! is_int($workspace)) {
return url('/admin');
}
$parameters = static::withoutLegacyScopeQuery($parameters);
$parameters['environment'] = $environment;
$parameters['workspace'] = $workspace instanceof Workspace
? static::workspaceRouteKey($workspace)
: $workspace;
return parent::getUrl($parameters, $isAbsolute, $panelId, null);
}
public static function canAccess(): bool
{
$environment = static::resolveRouteOwnedEnvironment();
$user = auth()->user();
if (! $environment instanceof ManagedEnvironment || ! $user instanceof User) {
return false;
}
if (! static::routeWorkspaceMatchesEnvironment($environment)) {
return false;
}
return app(ManagedEnvironmentAccessScopeResolver::class)
->decision($user, $environment, Capabilities::WORKSPACE_BASELINES_VIEW)
->allowed();
}
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
{
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::CrudListAndView)
->withListRowPrimaryActionLimit(1)
->satisfy(ActionSurfaceSlot::ListHeader, 'The page header exposes only the delegated compare rerun action and neutral navigation back to compare.')
->satisfy(ActionSurfaceSlot::ListRowMoreMenu, 'Row decisions are constrained to one primary binding action plus confirmed secondary decision/revoke actions.')
->satisfy(ActionSurfaceSlot::ListEmptyState, 'The table distinguishes no compare context from no decisions required.')
->exempt(ActionSurfaceSlot::DetailHeader, 'Spec 384 V1 does not add a separate detail route; subject context is shown in the row and confirmed action modals.')
->exempt(ActionSurfaceSlot::InspectAffordance, 'Rows expose compact candidate and decision context inline through the table and modal copy instead of a second detail route in v1.')
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'Baseline subject decisions are high-impact and intentionally have no bulk action in v1.');
}
public function mount(ManagedEnvironment|string|null $environment = null): void
{
$tenant = static::resolveRouteOwnedEnvironment($environment);
$this->authorizeEnvironmentOrAbort($tenant, Capabilities::WORKSPACE_BASELINES_VIEW);
$this->scopedEnvironmentId = (int) $tenant->getKey();
$this->focusedOperationRunId = $this->scopedOperationRunIdFromQuery($tenant);
$this->heading = $tenant->getFilamentName();
$this->subheading = 'Baseline subject resolution';
$this->mountInteractsWithTable();
}
public function table(Table $table): Table
{
return $table
->queryStringIdentifier('baselineSubjectResolution')
->defaultSort('readiness_impact')
->defaultPaginationPageOption(25)
->paginated(TablePaginationProfiles::customPage())
->searchable()
->searchPlaceholder('Search subject, reason, provider, decision')
->records(function (
?string $sortColumn,
?string $sortDirection,
?string $search,
array $filters,
int $page,
int $recordsPerPage
): LengthAwarePaginator {
$rows = $this->filteredRows($filters, $search);
$rows = $this->sortRows($rows, $sortColumn, $sortDirection);
return $this->paginateRows($rows, $page, $recordsPerPage);
})
->filters([
SelectFilter::make('provider')
->label('Provider')
->options(fn (): array => $this->filterOptions('provider')),
SelectFilter::make('subject_class')
->label('Class')
->options(fn (): array => $this->filterOptions('subject_class')),
SelectFilter::make('resource_type')
->label('Resource type')
->options(fn (): array => $this->filterOptions('resource_type')),
SelectFilter::make('actionability')
->label('Actionability')
->options(fn (): array => $this->filterOptions('actionability')),
SelectFilter::make('readiness_impact')
->label('Readiness')
->options(fn (): array => $this->filterOptions('readiness_impact')),
SelectFilter::make('reason')
->label('Reason')
->options(fn (): array => $this->filterOptions('reason')),
SelectFilter::make('active_binding')
->label('Decision')
->options([
'yes' => 'Decision recorded',
'no' => 'No decision',
]),
SelectFilter::make('candidates')
->label('Candidates')
->options([
'yes' => 'Has candidates',
'no' => 'No candidates',
]),
])
->columns([
TextColumn::make('subject_label')
->label('Subject')
->description(fn (Model $record): string => (string) $record->getAttribute('resource_type_label'))
->searchable()
->sortable()
->wrap(),
TextColumn::make('reason_label')
->label('Problem')
->badge()
->color('warning')
->searchable()
->sortable()
->wrap(),
TextColumn::make('readiness_label')
->label('Readiness')
->badge()
->color(fn (Model $record): string => $this->readinessColor((string) $record->getAttribute('readiness_impact')))
->sortable()
->wrap(),
TextColumn::make('actionability_label')
->label('Actionability')
->badge()
->color('gray')
->sortable()
->wrap(),
TextColumn::make('candidate_count')
->label('Candidates')
->numeric()
->sortable(),
TextColumn::make('current_decision_label')
->label('Current decision')
->badge()
->color(fn (Model $record): string => $record->getAttribute('active_binding_id') === null ? 'gray' : 'success')
->sortable()
->wrap(),
TextColumn::make('source_operation_run_id')
->label('Source')
->formatStateUsing(fn (mixed $state): string => is_numeric($state) ? 'Operation #'.(int) $state : 'Latest compare')
->sortable(),
TextColumn::make('provider_label')
->label('Provider')
->toggleable(isToggledHiddenByDefault: true)
->sortable(),
TextColumn::make('subject_class_label')
->label('Class')
->toggleable(isToggledHiddenByDefault: true)
->sortable(),
TextColumn::make('trust_level')
->label('Trust')
->toggleable(isToggledHiddenByDefault: true)
->formatStateUsing(fn (?string $state): string => $state !== null ? Str::of($state)->replace('_', ' ')->headline()->toString() : 'Unknown'),
])
->actions([
$this->bindSubjectAction(),
$this->recordDecisionAction(),
$this->revokeDecisionAction(),
])
->bulkActions([])
->emptyStateHeading(fn (): string => $this->emptyStateHeading())
->emptyStateDescription(fn (): string => $this->emptyStateDescription())
->emptyStateActions([
$this->runCompareEmptyStateAction(),
]);
}
/**
* @return array<string, mixed>
*/
protected function getViewData(): array
{
$tenant = $this->currentEnvironment();
$summary = $tenant instanceof ManagedEnvironment
? $this->query()->summary($tenant, $this->focusedOperationRunId)
: [
'has_run' => false,
'actionable_count' => 0,
'source_operation_run_id' => null,
'by_actionability' => [],
'by_readiness_impact' => [],
'by_reason' => [],
'legacy_payload_only' => false,
];
return [
'summary' => $summary,
'compareUrl' => $tenant instanceof ManagedEnvironment ? ManagedEnvironmentLinks::baselineCompareUrl($tenant) : null,
'sourceRunUrl' => $tenant instanceof ManagedEnvironment && is_numeric($summary['source_operation_run_id'] ?? null)
? OperationRunLinks::view((int) $summary['source_operation_run_id'], $tenant)
: null,
];
}
/**
* @return array<Action>
*/
protected function getHeaderActions(): array
{
$actions = [];
$tenant = $this->currentEnvironment();
if ($tenant instanceof ManagedEnvironment) {
$actions[] = Action::make('openBaselineCompare')
->label('Open baseline compare')
->icon('heroicon-o-arrow-left')
->color('gray')
->url(ManagedEnvironmentLinks::baselineCompareUrl($tenant));
}
$actions[] = $this->runCompareAction('runComparisonAgain');
return $actions;
}
public function currentEnvironment(): ?ManagedEnvironment
{
$tenant = $this->scopedEnvironmentId === null
? static::resolveRouteOwnedEnvironment()
: ManagedEnvironment::query()
->withTrashed()
->whereKey($this->scopedEnvironmentId)
->first();
if (! $tenant instanceof ManagedEnvironment) {
return null;
}
$this->authorizeEnvironmentOrAbort($tenant, Capabilities::WORKSPACE_BASELINES_VIEW);
return $tenant;
}
private function bindSubjectAction(): Action
{
$action = Action::make('bindSubject')
->label('Bind subject')
->icon('heroicon-o-link')
->visible(fn (Model $record): bool => (int) $record->getAttribute('candidate_count') > 0)
->form([
Select::make('candidate_key')
->label('Provider resource')
->required()
->native(false)
->options(fn (Model $record): array => collect($record->getAttribute('candidates') ?? [])
->mapWithKeys(fn (array $candidate): array => [
(string) $candidate['candidate_key'] => trim(implode(' - ', array_filter([
(string) ($candidate['display_label'] ?? 'Provider resource'),
(string) ($candidate['provider_label'] ?? ''),
(string) ($candidate['stable_identity_preview'] ?? ''),
]))),
])
->all()),
Textarea::make('operator_note')
->label('Operator note')
->required()
->minLength(8)
->rows(4)
->helperText('TenantPilot decision only. This does not mutate the provider tenant.'),
])
->requiresConfirmation()
->modalHeading('Bind subject')
->modalDescription('Record a TenantPilot-only subject binding. Future baseline compares can consume this active decision.')
->modalSubmitActionLabel('Bind subject')
->action(function (Model $record, array $data): void {
$tenant = $this->authorizedMutationEnvironment();
$actor = $this->actorOrAbort();
$row = $this->freshRowOrAbort($record);
$candidate = $this->candidateFromRow($row, (string) ($data['candidate_key'] ?? ''));
app(ProviderResourceBindingService::class)->createManualBinding(
actor: $actor,
environment: $tenant,
identity: ResourceIdentity::fromArray($candidate['identity']),
attributes: $this->bindingAttributes($row, $data, $candidate),
);
Notification::make()
->success()
->title('Subject binding recorded')
->body('Run baseline compare again to validate the decision against current evidence.')
->send();
$this->resetTable();
});
return UiEnforcement::forTableAction($action, fn (): ?ManagedEnvironment => $this->currentEnvironment())
->requireCapability(Capabilities::WORKSPACE_BASELINES_MANAGE)
->preserveVisibility()
->apply();
}
private function recordDecisionAction(): Action
{
$action = Action::make('recordDecision')
->label('Record decision')
->icon('heroicon-o-check-circle')
->color('gray')
->visible(fn (Model $record): bool => is_array($record->getAttribute('decision_identity')) && $record->getAttribute('active_binding_id') === null)
->form([
Select::make('decision')
->label('Decision')
->required()
->native(false)
->options([
'excluded_non_governed' => 'Exclude subject',
'accepted_limitation' => 'Accept limitation',
'unsupported_coverage' => 'Mark unsupported',
'missing_expected' => 'Mark missing expected',
]),
Textarea::make('operator_note')
->label('Operator note')
->required()
->minLength(8)
->rows(4)
->helperText('TenantPilot decision only. This does not mutate the provider tenant.'),
])
->requiresConfirmation()
->modalHeading('Record subject decision')
->modalDescription('Record a TenantPilot-only decision for this baseline subject. Future baseline compares can consume the active decision.')
->modalSubmitActionLabel('Record decision')
->action(function (Model $record, array $data): void {
$tenant = $this->authorizedMutationEnvironment();
$actor = $this->actorOrAbort();
$row = $this->freshRowOrAbort($record);
$identityPayload = $row['decision_identity'] ?? null;
if (! is_array($identityPayload)) {
throw new InvalidArgumentException('A valid provider resource identity is required for this decision.');
}
$identity = ResourceIdentity::fromArray($identityPayload);
$attributes = $this->bindingAttributes($row, $data);
match ((string) ($data['decision'] ?? '')) {
'excluded_non_governed' => app(ProviderResourceBindingService::class)->createExclusion($actor, $tenant, $identity, $attributes),
'accepted_limitation' => app(ProviderResourceBindingService::class)->createAcceptedLimitation($actor, $tenant, $identity, $attributes),
'unsupported_coverage' => app(ProviderResourceBindingService::class)->markUnsupported($actor, $tenant, $identity, $attributes),
'missing_expected' => app(ProviderResourceBindingService::class)->markMissingExpected($actor, $tenant, $identity, $attributes),
default => throw new InvalidArgumentException('Unsupported baseline subject decision.'),
};
Notification::make()
->success()
->title('Subject decision recorded')
->body('Run baseline compare again to validate the decision against current evidence.')
->send();
$this->resetTable();
});
return UiEnforcement::forTableAction($action, fn (): ?ManagedEnvironment => $this->currentEnvironment())
->requireCapability(Capabilities::WORKSPACE_BASELINES_MANAGE)
->preserveVisibility()
->apply();
}
private function revokeDecisionAction(): Action
{
$action = Action::make('revokeDecision')
->label('Revoke decision')
->icon('heroicon-o-no-symbol')
->color('danger')
->visible(fn (Model $record): bool => is_numeric($record->getAttribute('active_binding_id')))
->form([
Textarea::make('operator_note')
->label('Operator note')
->required()
->minLength(8)
->rows(4)
->helperText('TenantPilot decision only. This does not mutate the provider tenant.'),
])
->requiresConfirmation()
->modalHeading('Revoke subject decision')
->modalDescription('Revoke the active TenantPilot subject decision. Future baseline compares will no longer consume this decision.')
->modalSubmitActionLabel('Revoke decision')
->action(function (Model $record, array $data): void {
$this->authorizedMutationEnvironment();
$actor = $this->actorOrAbort();
$row = $this->freshRowOrAbort($record);
$bindingId = $row['active_binding_id'] ?? null;
if (! is_numeric($bindingId)) {
throw new NotFoundHttpException;
}
$binding = ProviderResourceBinding::query()
->whereKey((int) $bindingId)
->firstOrFail();
Gate::forUser($actor)->authorize('revoke', $binding);
app(ProviderResourceBindingService::class)->revoke(
actor: $actor,
binding: $binding,
operatorNote: (string) ($data['operator_note'] ?? ''),
resolutionReason: 'baseline_subject_resolution_revoked',
);
Notification::make()
->success()
->title('Subject decision revoked')
->body('Run baseline compare again to validate the updated decision state.')
->send();
$this->resetTable();
});
return UiEnforcement::forTableAction($action, fn (): ?ManagedEnvironment => $this->currentEnvironment())
->requireCapability(Capabilities::WORKSPACE_BASELINES_MANAGE)
->destructive()
->preserveVisibility()
->apply();
}
private function runCompareAction(string $name): Action
{
$label = 'Run comparison again';
$action = Action::make($name)
->label($label)
->icon('heroicon-o-arrow-path')
->requiresConfirmation()
->modalHeading($label)
->modalDescription('This delegates to the existing baseline compare start flow and queues a compare operation for the current environment.')
->modalSubmitActionLabel($label)
->action(function (): void {
$this->startBaselineCompare();
});
return UiEnforcement::forAction($action, fn (): ?ManagedEnvironment => $this->currentEnvironment())
->requireCapability(Capabilities::TENANT_SYNC)
->apply();
}
private function runCompareEmptyStateAction(): Action
{
return $this->runCompareAction('runComparisonAgainFromEmptyState')
->visible(fn (): bool => ! $this->summary()['has_run']);
}
private function startBaselineCompare(): void
{
$tenant = $this->currentEnvironment();
$user = auth()->user();
if (! $tenant instanceof ManagedEnvironment) {
Notification::make()->title('Open an environment to compare baselines')->danger()->send();
return;
}
if (! $user instanceof User) {
Notification::make()->title('Not authenticated')->danger()->send();
return;
}
$result = app(BaselineCompareService::class)->startCompare($tenant, $user);
if (! ($result['ok'] ?? false)) {
$reasonCode = is_string($result['reason_code'] ?? null) ? (string) $result['reason_code'] : 'unknown';
$translation = is_array($result['reason_translation'] ?? null) ? $result['reason_translation'] : [];
$message = is_string($translation['short_explanation'] ?? null) && trim((string) $translation['short_explanation']) !== ''
? trim((string) $translation['short_explanation'])
: 'Reason: '.$reasonCode;
Notification::make()
->title('Cannot start comparison')
->body($message)
->danger()
->send();
return;
}
$run = $result['run'] ?? null;
if ($run instanceof OperationRun) {
$this->focusedOperationRunId = (int) $run->getKey();
}
OpsUxBrowserEvents::dispatchRunEnqueued($this);
OperationUxPresenter::queuedToast($run instanceof OperationRun ? (string) $run->type : OperationRunType::BaselineCompare->value)
->actions($run instanceof OperationRun ? [
Action::make('view_run')
->label('Open operation')
->url(OperationRunLinks::view($run, $tenant)),
] : [])
->send();
$this->resetTable();
}
/**
* @return array<string, int|string|null>
*/
private function bindingAttributes(array $row, array $data, ?array $candidate = null): array
{
return [
'subject_domain' => (string) ($row['subject_domain'] ?? 'baseline'),
'subject_class' => (string) ($row['subject_class'] ?? 'policy_backed'),
'subject_type_key' => (string) ($row['subject_type_key'] ?? 'unknown'),
'canonical_subject_key' => is_string($row['canonical_subject_key'] ?? null) ? (string) $row['canonical_subject_key'] : null,
'display_label' => is_string($candidate['display_label'] ?? null)
? (string) $candidate['display_label']
: (string) ($row['subject_label'] ?? ''),
'resolution_reason' => (string) ($row['reason'] ?? 'baseline_subject_resolution'),
'operator_note' => (string) ($data['operator_note'] ?? ''),
'source_operation_run_id' => is_numeric($row['source_operation_run_id'] ?? null) ? (int) $row['source_operation_run_id'] : null,
'source_baseline_snapshot_id' => is_numeric($row['source_baseline_snapshot_id'] ?? null) ? (int) $row['source_baseline_snapshot_id'] : null,
'source_inventory_item_id' => is_numeric($candidate['source_inventory_item_id'] ?? $row['source_inventory_item_id'] ?? null)
? (int) ($candidate['source_inventory_item_id'] ?? $row['source_inventory_item_id'])
: null,
'source_policy_version_id' => is_numeric($candidate['source_policy_version_id'] ?? $row['source_policy_version_id'] ?? null)
? (int) ($candidate['source_policy_version_id'] ?? $row['source_policy_version_id'])
: null,
];
}
private function candidateFromRow(array $row, string $candidateKey): array
{
$candidate = collect($row['candidates'] ?? [])
->first(fn (array $candidate): bool => (string) ($candidate['candidate_key'] ?? '') === $candidateKey);
if (! is_array($candidate) || ! is_array($candidate['identity'] ?? null)) {
throw new InvalidArgumentException('Select a valid provider resource candidate.');
}
return $candidate;
}
private function freshRowOrAbort(Model $record): array
{
$tenant = $this->currentEnvironment();
if (! $tenant instanceof ManagedEnvironment) {
throw new NotFoundHttpException;
}
$row = $this->query()->row(
environment: $tenant,
rowId: (string) $record->getKey(),
filters: [
'operation_run_id' => $this->focusedOperationRunId,
'include_resolved' => true,
],
);
if (! is_array($row)) {
throw new NotFoundHttpException;
}
return $row;
}
private function authorizedMutationEnvironment(): ManagedEnvironment
{
$tenant = $this->currentEnvironment();
$this->authorizeEnvironmentOrAbort($tenant, Capabilities::WORKSPACE_BASELINES_MANAGE);
return $tenant;
}
private function actorOrAbort(): User
{
$user = auth()->user();
if (! $user instanceof User) {
abort(403);
}
return $user;
}
private function authorizeEnvironmentOrAbort(?ManagedEnvironment $environment, string $capability): void
{
$user = auth()->user();
if (! $environment instanceof ManagedEnvironment || ! $user instanceof User) {
abort(404);
}
if (! static::routeWorkspaceMatchesEnvironment($environment)) {
abort(404);
}
$decision = app(ManagedEnvironmentAccessScopeResolver::class)
->decision($user, $environment, $capability);
if ($decision->shouldDenyAsNotFound()) {
abort(404);
}
if ($decision->shouldDenyAsForbidden()) {
abort(403);
}
}
/**
* @param array<string, mixed> $filters
* @return Collection<int, array<string, mixed>>
*/
private function filteredRows(array $filters, ?string $search): Collection
{
$tenant = $this->currentEnvironment();
if (! $tenant instanceof ManagedEnvironment) {
return collect();
}
$normalizedFilters = [
'operation_run_id' => $this->focusedOperationRunId,
'provider' => data_get($filters, 'provider.value'),
'subject_class' => data_get($filters, 'subject_class.value'),
'resource_type' => data_get($filters, 'resource_type.value'),
'actionability' => data_get($filters, 'actionability.value'),
'readiness_impact' => data_get($filters, 'readiness_impact.value'),
'reason' => data_get($filters, 'reason.value'),
'active_binding' => data_get($filters, 'active_binding.value'),
'candidates' => data_get($filters, 'candidates.value'),
];
$rows = collect($this->query()->rows($tenant, $normalizedFilters));
$normalizedSearch = Str::lower(trim((string) $search));
if ($normalizedSearch === '') {
return $rows;
}
return $rows
->filter(fn (array $row): bool => str_contains((string) ($row['search_text'] ?? ''), $normalizedSearch))
->values();
}
/**
* @param Collection<int, array<string, mixed>> $rows
* @return Collection<int, array<string, mixed>>
*/
private function sortRows(Collection $rows, ?string $sortColumn, ?string $sortDirection): Collection
{
$allowed = [
'subject_label',
'reason_label',
'readiness_label',
'readiness_impact',
'actionability_label',
'candidate_count',
'current_decision_label',
'source_operation_run_id',
'provider_label',
'subject_class_label',
];
$sortColumn = in_array($sortColumn, $allowed, true) ? $sortColumn : 'readiness_impact';
$descending = Str::lower((string) ($sortDirection ?? 'asc')) === 'desc';
return $rows->sortBy(
fn (array $row): string|int => $sortColumn === 'candidate_count' || $sortColumn === 'source_operation_run_id'
? (int) ($row[$sortColumn] ?? 0)
: (string) ($row[$sortColumn] ?? ''),
SORT_NATURAL | SORT_FLAG_CASE,
$descending,
)->values();
}
/**
* @param Collection<int, array<string, mixed>> $rows
*/
private function paginateRows(Collection $rows, int $page, int $recordsPerPage): LengthAwarePaginator
{
$perPage = max(1, $recordsPerPage);
$currentPage = max(1, $page);
$items = $rows->forPage($currentPage, $perPage)
->values()
->map(fn (array $row): Model => $this->toTableRecord($row));
return new LengthAwarePaginator(
$items,
$rows->count(),
$perPage,
$currentPage,
);
}
/**
* @param array<string, mixed> $row
*/
private function toTableRecord(array $row): Model
{
$record = new class extends Model
{
public $timestamps = false;
public $incrementing = false;
protected $keyType = 'string';
protected $guarded = [];
protected $table = 'baseline_subject_resolution_rows';
};
$record->forceFill($row);
$record->exists = true;
return $record;
}
/**
* @return array<string, string>
*/
private function filterOptions(string $key): array
{
$tenant = $this->currentEnvironment();
if (! $tenant instanceof ManagedEnvironment) {
return [];
}
return $this->query()->filterOptions($tenant, $this->focusedOperationRunId, $key);
}
private function emptyStateHeading(): string
{
$summary = $this->summary();
if (! $summary['has_run']) {
return 'Run baseline compare first';
}
if ($summary['legacy_payload_only']) {
return 'Structured subject decisions unavailable';
}
return 'No baseline subject decisions required';
}
private function emptyStateDescription(): string
{
return match ($this->emptyStateHeading()) {
'Run baseline compare first' => 'No baseline compare run exists for this environment yet.',
'Structured subject decisions unavailable' => 'The latest compare run does not contain Spec 383 subject outcome semantics. Run baseline compare again to refresh the decision worklist.',
default => 'The selected compare context has no unresolved or decision-required baseline subjects.',
};
}
/**
* @return array<string, mixed>
*/
private function summary(): array
{
$tenant = $this->currentEnvironment();
if (! $tenant instanceof ManagedEnvironment) {
return ['has_run' => false, 'legacy_payload_only' => false, 'actionable_count' => 0];
}
return $this->query()->summary($tenant, $this->focusedOperationRunId);
}
private function readinessColor(string $readinessImpact): string
{
return match ($readinessImpact) {
'customer_blocker',
'internal_blocker' => 'danger',
'customer_limitation',
'internal_limitation' => 'warning',
'no_impact' => 'success',
default => 'gray',
};
}
private function scopedOperationRunIdFromQuery(ManagedEnvironment $tenant): ?int
{
$operationRunId = request()->query('operation_run_id');
if (! is_numeric($operationRunId)) {
return null;
}
$run = $this->query()->resolveRun($tenant, (int) $operationRunId);
return $run instanceof OperationRun ? (int) $run->getKey() : null;
}
protected static function resolveRouteOwnedEnvironment(ManagedEnvironment|string|null $environment = null): ?ManagedEnvironment
{
if ($environment instanceof ManagedEnvironment) {
return $environment;
}
if (is_string($environment) && $environment !== '') {
return ManagedEnvironment::query()
->where('slug', $environment)
->first();
}
$routeEnvironment = request()->route('environment') ?? request()->route('tenant');
if ($routeEnvironment instanceof ManagedEnvironment) {
return $routeEnvironment;
}
if (is_string($routeEnvironment) && $routeEnvironment !== '') {
return ManagedEnvironment::query()
->where('slug', $routeEnvironment)
->first();
}
$refererEnvironment = static::resolveRefererOwnedEnvironment();
if ($refererEnvironment instanceof ManagedEnvironment) {
return $refererEnvironment;
}
$filamentTenant = Filament::getTenant();
return $filamentTenant instanceof ManagedEnvironment ? $filamentTenant : null;
}
private static function resolveRefererOwnedEnvironment(): ?ManagedEnvironment
{
$referer = request()->headers->get('referer');
if (! is_string($referer) || $referer === '') {
return null;
}
$path = parse_url($referer, PHP_URL_PATH);
if (! is_string($path)) {
return null;
}
if (preg_match('#^/admin/workspaces/([^/]+)/environments/([^/]+)/baseline-subject-resolution$#', $path, $matches) !== 1) {
return null;
}
$workspaceRouteKey = rawurldecode($matches[1]);
$environmentRouteKey = rawurldecode($matches[2]);
$environment = ManagedEnvironment::query()
->where('slug', $environmentRouteKey)
->first();
if (! $environment instanceof ManagedEnvironment) {
return null;
}
$workspace = $environment->workspace instanceof Workspace
? $environment->workspace
: $environment->workspace()->first();
if (! $workspace instanceof Workspace) {
return null;
}
if ($workspaceRouteKey !== static::workspaceRouteKey($workspace) && $workspaceRouteKey !== (string) $workspace->getKey()) {
return null;
}
return $environment;
}
private static function routeWorkspaceMatchesEnvironment(ManagedEnvironment $environment): bool
{
$routeWorkspace = request()->route('workspace');
if ($routeWorkspace instanceof Workspace) {
return (int) $routeWorkspace->getKey() === (int) $environment->workspace_id;
}
if (! is_string($routeWorkspace) && ! is_int($routeWorkspace)) {
return true;
}
$routeWorkspace = trim((string) $routeWorkspace);
if ($routeWorkspace === '') {
return true;
}
$workspace = $environment->workspace instanceof Workspace
? $environment->workspace
: $environment->workspace()->first();
if (! $workspace instanceof Workspace) {
return false;
}
return $routeWorkspace === static::workspaceRouteKey($workspace)
|| $routeWorkspace === (string) $workspace->getKey();
}
/**
* @param array<mixed> $parameters
*/
protected static function resolveAdminUrlEnvironment(array $parameters, ?Model $tenant = null): ?ManagedEnvironment
{
if ($tenant instanceof ManagedEnvironment) {
return $tenant;
}
foreach (['environment', 'tenant', 'managed_environment_id', 'environment_id', 'tenant_id'] as $key) {
$value = $parameters[$key] ?? null;
if ($value instanceof ManagedEnvironment) {
return $value;
}
if (is_numeric($value)) {
return ManagedEnvironment::query()->whereKey((int) $value)->first();
}
if (is_string($value) && trim($value) !== '') {
return ManagedEnvironment::query()->where('slug', trim($value))->first();
}
}
return Filament::getTenant() instanceof ManagedEnvironment ? Filament::getTenant() : null;
}
/**
* @param array<mixed> $parameters
*/
protected static function resolveAdminUrlWorkspace(ManagedEnvironment $environment, array $parameters = []): Workspace|string|int|null
{
$workspace = $parameters['workspace'] ?? null;
if ($workspace instanceof Workspace || is_string($workspace) || is_int($workspace)) {
return $workspace;
}
return Workspace::query()->whereKey((int) $environment->workspace_id)->first();
}
protected static function workspaceRouteKey(Workspace $workspace): string
{
$slug = $workspace->getAttribute('slug');
return is_string($slug) && $slug !== ''
? $slug
: (string) $workspace->getKey();
}
/**
* @param array<mixed> $parameters
* @return array<mixed>
*/
protected static function withoutLegacyScopeQuery(array $parameters): array
{
foreach (self::LEGACY_SCOPE_QUERY_KEYS as $key) {
unset($parameters[$key]);
}
return $parameters;
}
private function query(): BaselineSubjectResolutionQuery
{
return app(BaselineSubjectResolutionQuery::class);
}
}