TenantAtlas/apps/platform/app/Filament/Pages/Monitoring/FindingExceptionsQueue.php
Ahmed Darrazi d427b8f54d
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m1s
feat: governance inbox final operator workflow (spec 346)
Implemented the final operator workflow for the Governance Inbox. This includes refactoring the inbox page, updating finding resources, adding UI enforcement policies, updating related blade views, and adding comprehensive tests for operator workflow and scope contracts.
2026-06-02 16:46:24 +02:00

883 lines
32 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Filament\Pages\Monitoring;
use App\Filament\Concerns\CleansAdminTenantQueryParameter;
use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState;
use App\Filament\Resources\FindingExceptionResource;
use App\Filament\Resources\FindingResource;
use App\Models\FindingException;
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Models\Workspace;
use App\Services\Auth\WorkspaceCapabilityResolver;
use App\Services\Findings\FindingExceptionService;
use App\Services\Findings\FindingRiskGovernanceResolver;
use App\Support\Auth\Capabilities;
use App\Support\Badges\BadgeDomain;
use App\Support\Badges\BadgeRenderer;
use App\Support\Filament\CanonicalAdminEnvironmentFilterState;
use App\Support\Filament\FilterOptionCatalog;
use App\Support\Filament\TablePaginationProfiles;
use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\Navigation\WorkspaceHubEnvironmentFilter;
use App\Support\Navigation\WorkspaceHubNavigation;
use App\Support\OperateHub\OperateHubShell;
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
use App\Support\Ui\ActionSurface\ActionSurfaceDefaults;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType;
use App\Support\Ui\GovernanceActions\GovernanceActionCatalog;
use App\Support\Workspaces\WorkspaceContext;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Facades\Filament;
use Filament\Forms\Components\DateTimePicker;
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\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use UnitEnum;
class FindingExceptionsQueue extends Page implements HasTable
{
use CleansAdminTenantQueryParameter;
use ClearsWorkspaceHubEnvironmentFilterState;
use InteractsWithTable;
protected const MONITORING_PAGE_STATE_CONTRACT = [
'surfaceKey' => 'finding_exceptions_queue',
'surfaceType' => 'selected_record_monitoring',
'stateFields' => [
[
'stateKey' => 'exception',
'stateClass' => 'inspect',
'carrier' => 'query_param',
'queryRole' => 'durable_restorable',
'shareable' => true,
'restorableOnRefresh' => true,
'tenantSensitive' => false,
'invalidFallback' => 'clear_selection_and_continue',
],
[
'stateKey' => 'environment_id',
'stateClass' => 'contextual_prefilter',
'carrier' => 'query_param',
'queryRole' => 'durable_restorable',
'shareable' => true,
'restorableOnRefresh' => true,
'tenantSensitive' => true,
'invalidFallback' => 'discard_and_continue',
],
[
'stateKey' => 'tableFilters',
'stateClass' => 'shareable_restorable',
'carrier' => 'session',
'queryRole' => 'unsupported',
'shareable' => false,
'restorableOnRefresh' => true,
'tenantSensitive' => true,
'invalidFallback' => 'discard_and_continue',
],
[
'stateKey' => 'tableSearch',
'stateClass' => 'shareable_restorable',
'carrier' => 'session',
'queryRole' => 'unsupported',
'shareable' => false,
'restorableOnRefresh' => true,
'tenantSensitive' => false,
'invalidFallback' => 'discard_and_continue',
],
],
'hydrationRule' => [
'precedenceOrder' => ['query', 'session', 'default'],
'appliesOnInitialMountOnly' => true,
'activeStateBecomesAuthoritativeAfterMount' => true,
'clearsOnTenantSwitch' => ['environment_id', 'managed_environment_id', 'status', 'current_validity_state'],
'invalidRequestedStateFallback' => 'clear_selection_and_continue',
],
'inspectContract' => [
'primaryModel' => FindingException::class,
'selectedStateKey' => 'selectedFindingExceptionId',
'openedBy' => ['query_param', 'inspect_action'],
'presentation' => 'summary_plus_related_actions',
'shareable' => true,
'invalidSelectionFallback' => 'clear_selection_and_continue',
],
'shareableStateKeys' => ['environment_id', 'exception'],
'localOnlyStateKeys' => [],
];
public ?int $selectedFindingExceptionId = null;
protected static bool $isDiscovered = false;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-shield-exclamation';
protected static string|UnitEnum|null $navigationGroup = 'Monitoring';
protected static ?string $navigationLabel = 'Finding exceptions';
protected static ?string $slug = 'finding-exceptions/queue';
protected static ?string $title = 'Finding Exceptions Queue';
protected string $view = 'filament.pages.monitoring.finding-exceptions-queue';
/**
* @var array<int, ManagedEnvironment>|null
*/
private ?array $authorizedTenants = null;
public static function getNavigationGroup(): string
{
return WorkspaceHubNavigation::workspaceWideGroup(__('localization.navigation.monitoring'));
}
public static function getNavigationUrl(): string
{
return WorkspaceHubNavigation::environmentFilteredUrl(static::getUrl(panel: 'admin'));
}
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
{
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::RunLog, ActionSurfaceType::QueueReview)
->withDefaults(new ActionSurfaceDefaults(
moreGroupLabel: 'More',
exportIsDefaultBulkActionForReadOnly: false,
))
->satisfy(ActionSurfaceSlot::ListHeader, 'Header actions keep workspace approval scope visible and expose selected exception review actions.')
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ViewAction->value)
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'Exception decisions are reviewed one record at a time in v1 and do not expose bulk actions.')
->satisfy(ActionSurfaceSlot::ListEmptyState, 'Empty state explains when the approval queue is empty and keeps navigation back to environment findings available.')
->satisfy(ActionSurfaceSlot::DetailHeader, 'Selected exception detail exposes approve, reject, and related-record navigation actions in the page header.');
}
/**
* @return array<string, mixed>
*/
public static function monitoringPageStateContract(): array
{
return self::MONITORING_PAGE_STATE_CONTRACT;
}
public static function canAccess(): bool
{
if (Filament::getCurrentPanel()?->getId() !== 'admin') {
return false;
}
$user = auth()->user();
if (! $user instanceof User) {
return false;
}
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
if (! is_int($workspaceId)) {
return false;
}
$workspace = Workspace::query()->whereKey($workspaceId)->first();
if (! $workspace instanceof Workspace) {
return false;
}
/** @var WorkspaceCapabilityResolver $resolver */
$resolver = app(WorkspaceCapabilityResolver::class);
return $resolver->isMember($user, $workspace)
&& $resolver->can($user, $workspace, Capabilities::FINDING_EXCEPTION_APPROVE);
}
public function mount(): void
{
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
$this->mountInteractsWithTable();
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
$this->applyRequestedTenantPrefilter();
$requestedExceptionId = is_numeric(request()->query('exception')) ? (int) request()->query('exception') : null;
if ($requestedExceptionId !== null) {
$this->selectedFindingExceptionId = $this->resolveSelectedFindingExceptionId($requestedExceptionId);
}
}
protected function getHeaderActions(): array
{
$actions = app(OperateHubShell::class)->headerActions(
scopeActionName: 'operate_hub_scope_finding_exceptions',
returnActionName: 'operate_hub_return_finding_exceptions',
);
$governanceContext = $this->incomingGovernanceContext();
if ($governanceContext?->backLinkUrl !== null) {
$actions[] = Action::make('return_to_governance_inbox')
->label($governanceContext->backLinkLabel ?? 'Back to governance inbox')
->icon('heroicon-o-arrow-left')
->color('gray')
->url($governanceContext->backLinkUrl);
}
$actions[] = Action::make('clear_filters')
->label('Clear filters')
->icon('heroicon-o-x-mark')
->color('gray')
->visible(fn (): bool => $this->hasActiveQueueFilters())
->action(fn (): mixed => $this->clearQueueFilters());
$actions[] = Action::make('view_tenant_register')
->label('View environment findings')
->icon('heroicon-o-arrow-top-right-on-square')
->color('gray')
->visible(fn (): bool => $this->filteredTenant() instanceof ManagedEnvironment)
->url(function (): ?string {
$tenant = $this->filteredTenant();
if (! $tenant instanceof ManagedEnvironment) {
return null;
}
return FindingExceptionResource::getUrl('index', tenant: $tenant);
});
$selectedContextActions = [
Action::make('clear_selected_exception')
->label('Exit focused review')
->icon('heroicon-o-x-mark')
->color('gray')
->visible(fn (): bool => $this->selectedFindingException() instanceof FindingException)
->url(fn (): string => $this->queueUrl(['exception' => null])),
Action::make('open_selected_exception')
->label('Open exception detail')
->icon('heroicon-o-arrow-top-right-on-square')
->color('gray')
->visible(fn (): bool => $this->selectedFindingException() instanceof FindingException)
->url(fn (): ?string => $this->selectedExceptionUrl()),
Action::make('open_selected_finding')
->label('Open finding')
->icon('heroicon-o-arrow-top-right-on-square')
->color('gray')
->visible(fn (): bool => $this->selectedFindingException() instanceof FindingException)
->url(fn (): ?string => $this->selectedFindingUrl()),
];
$selectedDecisionActions = [
Action::make('approve_selected_exception')
->label(GovernanceActionCatalog::rule('approve_exception')->canonicalLabel)
->color('success')
->visible(fn (): bool => $this->selectedFindingException()?->isPending() ?? false)
->requiresConfirmation()
->modalHeading(GovernanceActionCatalog::rule('approve_exception')->modalHeading)
->modalDescription(GovernanceActionCatalog::rule('approve_exception')->modalDescription)
->form([
DateTimePicker::make('effective_from')
->label('Effective from')
->required()
->seconds(false),
DateTimePicker::make('expires_at')
->label('Expires at')
->required()
->seconds(false),
Textarea::make('approval_reason')
->label('Approval reason')
->rows(3)
->required()
->maxLength(2000),
])
->action(function (array $data, FindingExceptionService $service): void {
$record = $this->selectedFindingException();
$user = auth()->user();
if (! $record instanceof FindingException || ! $user instanceof User) {
abort(404);
}
$wasRenewalRequest = $record->isPendingRenewal();
$updated = $service->approve($record, $user, $data);
$this->selectedFindingExceptionId = (int) $updated->getKey();
$this->resetTable();
Notification::make()
->title($wasRenewalRequest ? 'Exception renewed' : GovernanceActionCatalog::rule('approve_exception')->successTitle)
->success()
->send();
}),
Action::make('reject_selected_exception')
->label(GovernanceActionCatalog::rule('reject_exception')->canonicalLabel)
->color('warning')
->visible(fn (): bool => $this->selectedFindingException()?->isPending() ?? false)
->requiresConfirmation()
->modalHeading(GovernanceActionCatalog::rule('reject_exception')->modalHeading)
->modalDescription(GovernanceActionCatalog::rule('reject_exception')->modalDescription)
->form([
Textarea::make('rejection_reason')
->label('Rejection reason')
->rows(3)
->required()
->maxLength(2000),
])
->action(function (array $data, FindingExceptionService $service): void {
$record = $this->selectedFindingException();
$user = auth()->user();
if (! $record instanceof FindingException || ! $user instanceof User) {
abort(404);
}
$wasRenewalRequest = $record->isPendingRenewal();
$updated = $service->reject($record, $user, $data);
$this->selectedFindingExceptionId = (int) $updated->getKey();
$this->resetTable();
Notification::make()
->title($wasRenewalRequest ? 'Renewal rejected' : GovernanceActionCatalog::rule('reject_exception')->successTitle)
->success()
->send();
}),
];
$actions[] = ActionGroup::make($selectedContextActions)
->label('Focused review')
->icon('heroicon-o-rectangle-stack')
->color('gray')
->visible(fn (): bool => $this->selectedFindingException() instanceof FindingException);
$actions[] = ActionGroup::make($selectedDecisionActions)
->label('Review selected')
->icon('heroicon-o-shield-check')
->color('primary')
->visible(fn (): bool => $this->selectedFindingException()?->isPending() ?? false);
return $actions;
}
public function table(Table $table): Table
{
return $table
->query(fn (): Builder => $this->queueBaseQuery())
->defaultSort('requested_at', 'asc')
->paginated(TablePaginationProfiles::customPage())
->persistFiltersInSession()
->persistSearchInSession()
->persistSortInSession()
->columns([
TextColumn::make('status')
->badge()
->formatStateUsing(BadgeRenderer::label(BadgeDomain::FindingExceptionStatus))
->color(BadgeRenderer::color(BadgeDomain::FindingExceptionStatus))
->icon(BadgeRenderer::icon(BadgeDomain::FindingExceptionStatus))
->iconColor(BadgeRenderer::iconColor(BadgeDomain::FindingExceptionStatus)),
TextColumn::make('current_validity_state')
->label('Validity')
->badge()
->formatStateUsing(BadgeRenderer::label(BadgeDomain::FindingRiskGovernanceValidity))
->color(BadgeRenderer::color(BadgeDomain::FindingRiskGovernanceValidity))
->icon(BadgeRenderer::icon(BadgeDomain::FindingRiskGovernanceValidity))
->iconColor(BadgeRenderer::iconColor(BadgeDomain::FindingRiskGovernanceValidity)),
TextColumn::make('tenant.name')
->label('Environment')
->searchable(),
TextColumn::make('finding_summary')
->label('Finding')
->state(fn (FindingException $record): string => $record->finding?->resolvedSubjectDisplayName() ?: 'Finding #'.$record->finding_id)
->searchable(),
TextColumn::make('governance_warning')
->label('Governance warning')
->state(fn (FindingException $record): ?string => $this->governanceWarning($record))
->color(fn (FindingException $record): string => $this->governanceWarningColor($record))
->wrap(),
TextColumn::make('requester.name')
->label('Requested by')
->placeholder('—'),
TextColumn::make('owner.name')
->label('Owner')
->placeholder('—'),
TextColumn::make('review_due_at')
->label('Review due')
->dateTime()
->placeholder('—')
->sortable(),
TextColumn::make('expires_at')
->label('Expires')
->dateTime()
->placeholder('—')
->sortable(),
TextColumn::make('requested_at')
->label('Requested')
->dateTime()
->sortable(),
])
->filters([
SelectFilter::make('managed_environment_id')
->label('Environment')
->options(fn (): array => $this->tenantFilterOptions())
->searchable(),
SelectFilter::make('status')
->options(FilterOptionCatalog::findingExceptionStatuses()),
SelectFilter::make('current_validity_state')
->label('Validity')
->options(FilterOptionCatalog::findingExceptionValidityStates()),
])
->actions([
Action::make('inspect_exception')
->label('Inspect exception')
->icon('heroicon-o-eye')
->color('gray')
->url(fn (FindingException $record): string => $this->queueUrl(['exception' => (int) $record->getKey()])),
])
->bulkActions([])
->emptyStateHeading('No exceptions match this queue')
->emptyStateDescription('Adjust the current environment or lifecycle filters to review governed exceptions in this workspace.')
->emptyStateIcon('heroicon-o-shield-check')
->emptyStateActions([
Action::make('clear_filters')
->label('Clear filters')
->icon('heroicon-o-x-mark')
->color('gray')
->action(fn (): mixed => $this->clearQueueFilters()),
]);
}
public function updatedTableFilters(): void
{
$this->normalizeSelectedFindingExceptionId();
}
public function updatedTableSearch(): void
{
$this->normalizeSelectedFindingExceptionId();
}
public function selectedFindingException(): ?FindingException
{
if (! is_int($this->selectedFindingExceptionId)) {
return null;
}
$this->normalizeSelectedFindingExceptionId();
if (! is_int($this->selectedFindingExceptionId)) {
return null;
}
try {
return $this->resolveSelectedFindingException($this->selectedFindingExceptionId);
} catch (NotFoundHttpException) {
return null;
}
}
public function selectedExceptionUrl(): ?string
{
$record = $this->selectedFindingException();
if (! $record instanceof FindingException || ! $record->tenant) {
return null;
}
return $this->appendQuery(
FindingExceptionResource::getUrl('view', ['record' => $record], tenant: $record->tenant),
$this->navigationContext()?->toQuery() ?? [],
);
}
public function selectedFindingUrl(): ?string
{
$record = $this->selectedFindingException();
if (! $record instanceof FindingException || ! $record->finding || ! $record->tenant) {
return null;
}
return $this->appendQuery(
FindingResource::getUrl('view', ['record' => $record->finding], tenant: $record->tenant),
$this->navigationContext()?->toQuery() ?? [],
);
}
public function clearSelectedException(): void
{
$this->selectedFindingExceptionId = null;
}
public function clearQueueFilters(): void
{
$hadEnvironmentFilter = $this->currentTenantFilterId() !== null;
$this->removeTableFilter('managed_environment_id');
$this->removeTableFilter('status');
$this->removeTableFilter('current_validity_state');
$this->selectedFindingExceptionId = null;
$this->clearWorkspaceHubEnvironmentFilterState(request());
$this->resetTable();
if ($hadEnvironmentFilter) {
$this->redirectToCleanWorkspaceHubUrl(static::getUrl(panel: 'admin'), request());
}
}
/**
* @return array{label: string, clear_url: string}|null
*/
public function environmentFilterChip(): ?array
{
$tenant = $this->filteredTenant();
if (! $tenant instanceof ManagedEnvironment) {
return null;
}
return [
'label' => (string) $tenant->name,
'clear_url' => $this->cleanWorkspaceHubUrl(static::getUrl(panel: 'admin')),
];
}
/**
* @return array<int, ManagedEnvironment>
*/
public function authorizedTenants(): array
{
if ($this->authorizedTenants !== null) {
return $this->authorizedTenants;
}
$user = auth()->user();
if (! $user instanceof User) {
return $this->authorizedTenants = [];
}
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
if (! is_int($workspaceId)) {
return $this->authorizedTenants = [];
}
$tenants = $user->accessibleManagedEnvironmentsQuery($workspaceId)
->orderBy('managed_environments.name')
->get();
return $this->authorizedTenants = $tenants
->filter(fn (ManagedEnvironment $tenant): bool => (int) $tenant->workspace_id === $workspaceId)
->values()
->all();
}
private function queueBaseQuery(): Builder
{
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
$tenantIds = array_values(array_map(
static fn (ManagedEnvironment $tenant): int => (int) $tenant->getKey(),
$this->authorizedTenants(),
));
return FindingException::query()
->with([
'tenant',
'requester',
'owner',
'approver',
'finding' => fn ($query) => $query->withSubjectDisplayName(),
'decisions.actor',
'evidenceReferences',
])
->where('workspace_id', (int) $workspaceId)
->whereIn('managed_environment_id', $tenantIds === [] ? [-1] : $tenantIds);
}
/**
* @return array<string, string>
*/
private function tenantFilterOptions(): array
{
return Collection::make($this->authorizedTenants())
->mapWithKeys(static fn (ManagedEnvironment $tenant): array => [
(string) $tenant->getKey() => $tenant->name,
])
->all();
}
private function applyRequestedTenantPrefilter(): void
{
$workspace = $this->currentWorkspace();
if (! $workspace instanceof Workspace) {
return;
}
$filter = WorkspaceHubEnvironmentFilter::fromRequest(request(), $workspace);
if (! $filter instanceof WorkspaceHubEnvironmentFilter) {
return;
}
$environmentId = $filter->environmentId();
foreach ($this->authorizedTenants() as $tenant) {
if ((int) $tenant->getKey() === $environmentId) {
$this->tableFilters['managed_environment_id']['value'] = (string) $environmentId;
$this->tableDeferredFilters['managed_environment_id']['value'] = (string) $environmentId;
return;
}
}
throw new NotFoundHttpException;
}
private function filteredTenant(): ?ManagedEnvironment
{
$tenantId = $this->currentTenantFilterId();
if (! is_int($tenantId)) {
return null;
}
foreach ($this->authorizedTenants() as $tenant) {
if ((int) $tenant->getKey() === $tenantId) {
return $tenant;
}
}
return null;
}
private function currentTenantFilterId(): ?int
{
$tenantFilter = app(CanonicalAdminEnvironmentFilterState::class)->currentFilterValue(
$this->getTableFiltersSessionKey(),
$this->tableFilters ?? [],
request(),
);
return is_numeric($tenantFilter) ? (int) $tenantFilter : null;
}
private function hasActiveQueueFilters(): bool
{
return $this->currentTenantFilterId() !== null
|| is_string(data_get($this->tableFilters, 'status.value'))
|| is_string(data_get($this->tableFilters, 'current_validity_state.value'));
}
private function resolveSelectedFindingException(int $findingExceptionId): FindingException
{
$record = $this->queueBaseQuery()
->whereKey($findingExceptionId)
->first();
if (! $record instanceof FindingException) {
throw new NotFoundHttpException;
}
return $record;
}
private function queueUrl(array $overrides = []): string
{
$parameters = array_merge(
$this->navigationContext()?->toQuery() ?? [],
[
'environment_id' => $this->filteredTenant()?->getKey(),
'exception' => $this->selectedFindingExceptionId,
],
$overrides,
);
return static::getUrl(
panel: 'admin',
parameters: array_filter($parameters, static fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []),
);
}
private function currentWorkspace(): ?Workspace
{
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
if (! is_int($workspaceId)) {
return null;
}
return Workspace::query()->whereKey($workspaceId)->first();
}
private function navigationContext(): ?CanonicalNavigationContext
{
return CanonicalNavigationContext::fromRequest(request());
}
private function incomingGovernanceContext(): ?CanonicalNavigationContext
{
$context = $this->navigationContext();
return $context?->sourceSurface === 'governance.inbox'
? $context
: null;
}
private function normalizeSelectedFindingExceptionId(): void
{
if (! is_int($this->selectedFindingExceptionId) || $this->selectedFindingExceptionId <= 0) {
$this->selectedFindingExceptionId = null;
return;
}
$this->selectedFindingExceptionId = $this->resolveSelectedFindingExceptionId($this->selectedFindingExceptionId);
}
private function resolveSelectedFindingExceptionId(int $findingExceptionId): ?int
{
try {
$record = $this->resolveSelectedFindingException($findingExceptionId);
} catch (NotFoundHttpException) {
return null;
}
return $this->selectedFindingExceptionVisible((int) $record->getKey())
? (int) $record->getKey()
: null;
}
private function selectedFindingExceptionVisible(int $findingExceptionId): bool
{
$record = $this->resolveSelectedFindingException($findingExceptionId);
return $this->matchesSelectedFindingExceptionFilters($record)
&& $this->matchesSelectedFindingExceptionSearch($record);
}
/**
* @return array<string, mixed>
*/
private function currentQueueFiltersState(): array
{
$persisted = session()->get($this->getTableFiltersSessionKey(), []);
return array_replace_recursive(
is_array($persisted) ? $persisted : [],
$this->tableFilters ?? [],
);
}
private function currentQueueSearchState(): string
{
$search = trim((string) ($this->tableSearch ?? ''));
if ($search !== '') {
return $search;
}
$persisted = session()->get($this->getTableSearchSessionKey(), '');
return trim(is_string($persisted) ? $persisted : '');
}
private function matchesSelectedFindingExceptionFilters(FindingException $record): bool
{
$filters = $this->currentQueueFiltersState();
$tenantFilter = data_get($filters, 'managed_environment_id.value');
if (is_numeric($tenantFilter) && (int) $record->managed_environment_id !== (int) $tenantFilter) {
return false;
}
$statusFilter = data_get($filters, 'status.value');
if (is_string($statusFilter) && $statusFilter !== '' && (string) $record->status !== $statusFilter) {
return false;
}
$validityFilter = data_get($filters, 'current_validity_state.value');
if (is_string($validityFilter) && $validityFilter !== '' && (string) $record->current_validity_state !== $validityFilter) {
return false;
}
return true;
}
private function matchesSelectedFindingExceptionSearch(FindingException $record): bool
{
$search = Str::lower($this->currentQueueSearchState());
if ($search === '') {
return true;
}
$haystack = Str::lower(implode(' ', [
$record->tenant?->name ?? '',
$record->finding?->resolvedSubjectDisplayName() ?? 'Finding #'.$record->finding_id,
$record->request_reason ?? '',
]));
return str_contains($haystack, $search);
}
private function governanceWarning(FindingException $record): ?string
{
$finding = $record->relationLoaded('finding')
? $record->finding
: $record->finding()->withSubjectDisplayName()->first();
if (! $finding instanceof \App\Models\Finding) {
return null;
}
return app(FindingRiskGovernanceResolver::class)->resolveWarningMessage($finding, $record);
}
private function governanceWarningColor(FindingException $record): string
{
if ((string) $record->current_validity_state === FindingException::VALIDITY_EXPIRING) {
return 'warning';
}
$finding = $record->relationLoaded('finding')
? $record->finding
: $record->finding()->withSubjectDisplayName()->first();
if ($finding instanceof \App\Models\Finding && $record->requiresFreshDecisionForFinding($finding)) {
return 'warning';
}
return 'danger';
}
/**
* @param array<string, mixed> $query
*/
private function appendQuery(string $url, array $query): string
{
if ($query === []) {
return $url;
}
return $url.(str_contains($url, '?') ? '&' : '?').http_build_query($query);
}
}