779 lines
33 KiB
PHP
779 lines
33 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\Resources\EnvironmentReviewResource\Pages;
|
|
|
|
use App\Filament\Resources\EnvironmentReviewResource;
|
|
use App\Filament\Resources\EvidenceSnapshotResource;
|
|
use App\Filament\Resources\ReviewPackResource;
|
|
use App\Filament\Resources\StoredReportResource;
|
|
use App\Models\EnvironmentReview;
|
|
use App\Models\EvidenceSnapshot;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\OperationRun;
|
|
use App\Models\ReviewPack;
|
|
use App\Models\ReviewPublicationResolutionCase;
|
|
use App\Models\ReviewPublicationResolutionStep;
|
|
use App\Models\StoredReport;
|
|
use App\Models\User;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\Auth\UiTooltips as AuthUiTooltips;
|
|
use App\Support\OperationRunLinks;
|
|
use App\Support\OpsUx\OperationUxPresenter;
|
|
use App\Support\Rbac\UiEnforcement;
|
|
use App\Support\ReviewPublicationResolution\ResolutionProofCurrentness;
|
|
use App\Support\ReviewPublicationResolution\ResolutionProofEvaluation;
|
|
use App\Support\ReviewPublicationResolution\ResolutionProofUsability;
|
|
use App\Support\ReviewPublicationResolution\ResolutionProofVisibility;
|
|
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionActionService;
|
|
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionCaseStatus;
|
|
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionService;
|
|
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionStepAuthorizer;
|
|
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionStepKey;
|
|
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionStepStatus;
|
|
use Filament\Actions;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Resources\Pages\Concerns\InteractsWithRecord;
|
|
use Filament\Resources\Pages\Page;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Livewire\Attributes\Locked;
|
|
|
|
class ResolveReviewPublication extends Page
|
|
{
|
|
use InteractsWithRecord;
|
|
|
|
protected static string $resource = EnvironmentReviewResource::class;
|
|
|
|
protected string $view = 'filament.resources.environment-review-resource.pages.resolve-review-publication';
|
|
|
|
#[Locked]
|
|
public ?int $resolutionCaseId = null;
|
|
|
|
public function mount(int|string $record): void
|
|
{
|
|
$this->record = $this->resolveRecord($record);
|
|
$this->authorizeAccess();
|
|
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User || ! $this->record instanceof EnvironmentReview) {
|
|
abort(403);
|
|
}
|
|
|
|
$caseService = app(ReviewPublicationResolutionService::class);
|
|
$case = $user->can('refresh', $this->record)
|
|
? $caseService->openOrResume(
|
|
review: $this->record,
|
|
actor: $user,
|
|
sourceSurface: 'environment_review_detail',
|
|
)
|
|
: $caseService->activeCaseForReview($this->record);
|
|
|
|
if ($case instanceof ReviewPublicationResolutionCase && ! $user->can('view', $case)) {
|
|
abort(404);
|
|
}
|
|
|
|
if (! $user->can('refresh', $this->record) && ! ($case instanceof ReviewPublicationResolutionCase)) {
|
|
abort(403);
|
|
}
|
|
|
|
if (! $case instanceof ReviewPublicationResolutionCase) {
|
|
Notification::make()
|
|
->success()
|
|
->title('Review is ready for publication')
|
|
->body('No publication resolution case was needed for the current review state.')
|
|
->send();
|
|
|
|
$this->redirect(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $this->record], $this->record->tenant));
|
|
|
|
return;
|
|
}
|
|
|
|
$this->resolutionCaseId = (int) $case->getKey();
|
|
$this->heading = $case->status === ReviewPublicationResolutionCaseStatus::ReadyToContinue->value
|
|
? 'Review is ready to continue'
|
|
: 'Review can\'t be published yet';
|
|
$this->subheading = $this->record->tenant?->getFilamentName();
|
|
}
|
|
|
|
public function getTitle(): string
|
|
{
|
|
return $this->pageOutcomeTitle();
|
|
}
|
|
|
|
public function getHeading(): string
|
|
{
|
|
return $this->pageOutcomeTitle();
|
|
}
|
|
|
|
protected function resolveRecord(int|string $key): Model
|
|
{
|
|
return EnvironmentReviewResource::resolveScopedRecordOrFail($key);
|
|
}
|
|
|
|
protected function getHeaderActions(): array
|
|
{
|
|
return array_values(array_filter([
|
|
$this->executeCurrentStepAction(),
|
|
Actions\Action::make('back_to_review')
|
|
->label('Return to review')
|
|
->icon('heroicon-o-arrow-left')
|
|
->color('gray')
|
|
->url(fn (): string => $this->reviewUrl()),
|
|
Actions\ActionGroup::make([
|
|
$this->cancelResolutionAction(),
|
|
])
|
|
->label('More')
|
|
->icon('heroicon-o-ellipsis-vertical')
|
|
->color('gray')
|
|
->visible(fn (): bool => $this->resolutionCase()?->statusEnum()->isActive() ?? false),
|
|
]));
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function caseState(): array
|
|
{
|
|
$case = $this->resolutionCase();
|
|
|
|
if (! $case instanceof ReviewPublicationResolutionCase) {
|
|
return [
|
|
'case' => null,
|
|
'steps' => [],
|
|
];
|
|
}
|
|
|
|
$case->loadMissing(['steps.operationRun', 'environmentReview.tenant', 'tenant']);
|
|
$tenant = $case->tenant;
|
|
$currentStep = $case->currentStep();
|
|
|
|
return [
|
|
'case' => [
|
|
'id' => (int) $case->getKey(),
|
|
'status' => (string) $case->status,
|
|
'status_label' => $this->statusLabel((string) $case->status),
|
|
'status_color' => $this->caseStatusColor((string) $case->status),
|
|
'current_step_key' => $case->current_step_key,
|
|
'summary' => is_array($case->summary) ? $case->summary : [],
|
|
'created_at' => $case->created_at?->diffForHumans(),
|
|
'last_evaluated_at' => $case->last_evaluated_at?->diffForHumans(),
|
|
],
|
|
'decision' => $this->decisionState($case, $currentStep),
|
|
'steps' => $case->steps
|
|
->map(fn (ReviewPublicationResolutionStep $step): array => $this->stepState($step, $tenant))
|
|
->values()
|
|
->all(),
|
|
];
|
|
}
|
|
|
|
private function executeCurrentStepAction(): Actions\Action
|
|
{
|
|
$action = Actions\Action::make('execute_current_step')
|
|
->label(fn (): string => $this->currentStepActionLabel())
|
|
->icon(fn (): string => $this->currentStepActionIcon())
|
|
->color('primary')
|
|
->visible(fn (): bool => $this->hasExecutableCurrentStep() && ! $this->currentStepIsRunning())
|
|
->disabled(fn (): bool => $this->currentStepIsRunning() || ! $this->canExecuteCurrentStep())
|
|
->tooltip(fn (): ?string => $this->currentStepActionTooltip())
|
|
->requiresConfirmation()
|
|
->modalHeading(fn (): string => $this->currentStepConfirmationHeading())
|
|
->modalDescription(fn (): string => $this->currentStepConfirmationDescription())
|
|
->modalSubmitActionLabel(fn (): string => $this->currentStepActionLabel())
|
|
->action(function (): void {
|
|
$case = $this->resolutionCase();
|
|
$user = auth()->user();
|
|
|
|
if (! $case instanceof ReviewPublicationResolutionCase || ! $user instanceof User) {
|
|
abort(403);
|
|
}
|
|
|
|
if (! $this->canExecuteCurrentStep()) {
|
|
abort(403);
|
|
}
|
|
|
|
$result = app(ReviewPublicationResolutionActionService::class)->executeCurrentStep($case, $user);
|
|
$updatedCase = $result['case'];
|
|
$this->resolutionCaseId = (int) $updatedCase->getKey();
|
|
|
|
if ($result['operation_run'] instanceof OperationRun && is_string($result['operation_type'])) {
|
|
OperationUxPresenter::queuedToast($result['operation_type'])->send();
|
|
|
|
return;
|
|
}
|
|
|
|
Notification::make()
|
|
->success()
|
|
->title('Resolution step completed')
|
|
->send();
|
|
|
|
if ($updatedCase->status === ReviewPublicationResolutionCaseStatus::Completed->value) {
|
|
$this->redirect($this->reviewUrl());
|
|
}
|
|
});
|
|
|
|
return $action;
|
|
}
|
|
|
|
private function cancelResolutionAction(): Actions\Action
|
|
{
|
|
return UiEnforcement::forAction(
|
|
Actions\Action::make('cancel_resolution')
|
|
->label('Cancel resolution')
|
|
->icon('heroicon-o-x-circle')
|
|
->color('danger')
|
|
->visible(fn (): bool => $this->resolutionCase()?->statusEnum()->isActive() ?? false)
|
|
->requiresConfirmation()
|
|
->modalHeading('Cancel publication resolution')
|
|
->modalDescription('This only cancels the TenantPilot resolution case. It does not modify the provider tenant or publish the review.')
|
|
->action(function (): void {
|
|
$case = $this->resolutionCase();
|
|
$user = auth()->user();
|
|
|
|
if (! $case instanceof ReviewPublicationResolutionCase || ! $user instanceof User) {
|
|
abort(403);
|
|
}
|
|
|
|
app(ReviewPublicationResolutionService::class)->cancel($case, $user);
|
|
|
|
Notification::make()
|
|
->success()
|
|
->title('Resolution cancelled')
|
|
->send();
|
|
|
|
$this->redirect($this->reviewUrl());
|
|
}),
|
|
)
|
|
->requireCapability(Capabilities::ENVIRONMENT_REVIEW_MANAGE)
|
|
->preserveVisibility()
|
|
->apply();
|
|
}
|
|
|
|
private function authorizeAccess(): void
|
|
{
|
|
$tenant = EnvironmentReviewResource::panelTenantContext();
|
|
$record = $this->getRecord();
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User || ! $tenant instanceof ManagedEnvironment || ! $record instanceof EnvironmentReview) {
|
|
abort(404);
|
|
}
|
|
|
|
if ((int) $record->managed_environment_id !== (int) $tenant->getKey()) {
|
|
abort(404);
|
|
}
|
|
|
|
if (! $user->canAccessTenant($tenant)) {
|
|
abort(404);
|
|
}
|
|
|
|
if (! $user->can('view', $record)) {
|
|
abort(403);
|
|
}
|
|
}
|
|
|
|
private function resolutionCase(): ?ReviewPublicationResolutionCase
|
|
{
|
|
if (! is_numeric($this->resolutionCaseId)) {
|
|
return null;
|
|
}
|
|
|
|
$case = ReviewPublicationResolutionCase::query()
|
|
->with(['steps.operationRun', 'environmentReview.tenant', 'tenant'])
|
|
->find((int) $this->resolutionCaseId);
|
|
|
|
if (! $case instanceof ReviewPublicationResolutionCase) {
|
|
return null;
|
|
}
|
|
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User || ! $user->can('view', $case)) {
|
|
abort(403);
|
|
}
|
|
|
|
return $case;
|
|
}
|
|
|
|
private function pageOutcomeTitle(): string
|
|
{
|
|
$case = $this->resolutionCase();
|
|
|
|
if ($case instanceof ReviewPublicationResolutionCase
|
|
&& $case->status === ReviewPublicationResolutionCaseStatus::ReadyToContinue->value) {
|
|
return 'Review is ready to continue';
|
|
}
|
|
|
|
return 'Review can\'t be published yet';
|
|
}
|
|
|
|
private function currentStep(): ?ReviewPublicationResolutionStep
|
|
{
|
|
return $this->resolutionCase()?->currentStep();
|
|
}
|
|
|
|
private function hasExecutableCurrentStep(): bool
|
|
{
|
|
$step = $this->currentStep();
|
|
|
|
if (! $step instanceof ReviewPublicationResolutionStep) {
|
|
return false;
|
|
}
|
|
|
|
return $step->statusEnum() !== ReviewPublicationResolutionStepStatus::Pending;
|
|
}
|
|
|
|
private function currentStepIsRunning(): bool
|
|
{
|
|
return $this->currentStep()?->statusEnum() === ReviewPublicationResolutionStepStatus::Running;
|
|
}
|
|
|
|
private function canExecuteCurrentStep(): bool
|
|
{
|
|
$case = $this->resolutionCase();
|
|
$user = auth()->user();
|
|
|
|
if (! $case instanceof ReviewPublicationResolutionCase || ! $user instanceof User) {
|
|
return false;
|
|
}
|
|
|
|
return app(ReviewPublicationResolutionStepAuthorizer::class)->canExecuteCurrentStep($user, $case);
|
|
}
|
|
|
|
private function currentStepActionTooltip(): ?string
|
|
{
|
|
if ($this->currentStepIsRunning()) {
|
|
return 'The linked operation is still running.';
|
|
}
|
|
|
|
return $this->canExecuteCurrentStep() ? null : AuthUiTooltips::insufficientPermission();
|
|
}
|
|
|
|
private function currentStepActionLabel(): string
|
|
{
|
|
return $this->currentStepActionLabelFor($this->currentStep()?->stepKeyEnum());
|
|
}
|
|
|
|
private function currentStepActionLabelFor(?ReviewPublicationResolutionStepKey $stepKey): string
|
|
{
|
|
return match ($stepKey) {
|
|
ReviewPublicationResolutionStepKey::CompleteRequiredReports => 'Update required reports',
|
|
ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot => 'Collect evidence',
|
|
ReviewPublicationResolutionStepKey::RefreshReviewComposition => 'Refresh review',
|
|
ReviewPublicationResolutionStepKey::GenerateReviewPack => 'Prepare export',
|
|
ReviewPublicationResolutionStepKey::ReturnToPublication => 'Return to review',
|
|
default => 'Continue',
|
|
};
|
|
}
|
|
|
|
private function currentStepActionIcon(): string
|
|
{
|
|
return $this->currentStepActionIconFor($this->currentStep()?->stepKeyEnum());
|
|
}
|
|
|
|
private function currentStepActionIconFor(?ReviewPublicationResolutionStepKey $stepKey): string
|
|
{
|
|
return match ($stepKey) {
|
|
ReviewPublicationResolutionStepKey::CompleteRequiredReports,
|
|
ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot,
|
|
ReviewPublicationResolutionStepKey::RefreshReviewComposition => 'heroicon-o-arrow-path',
|
|
ReviewPublicationResolutionStepKey::GenerateReviewPack => 'heroicon-o-arrow-down-tray',
|
|
ReviewPublicationResolutionStepKey::ReturnToPublication => 'heroicon-o-check-badge',
|
|
default => 'heroicon-o-play',
|
|
};
|
|
}
|
|
|
|
private function reviewUrl(): string
|
|
{
|
|
$record = $this->getRecord();
|
|
|
|
if (! $record instanceof EnvironmentReview) {
|
|
return url('/admin');
|
|
}
|
|
|
|
return EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $record], $record->tenant);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function decisionState(ReviewPublicationResolutionCase $case, ?ReviewPublicationResolutionStep $currentStep): array
|
|
{
|
|
$summary = is_array($case->summary) ? $case->summary : [];
|
|
$missingReports = collect((array) ($summary['missing_report_dimensions'] ?? []))
|
|
->map(fn (mixed $dimension): string => $this->reportRequirementLabel((string) $dimension))
|
|
->filter(static fn (string $dimension): bool => $dimension !== '')
|
|
->values()
|
|
->all();
|
|
$blockers = collect((array) ($summary['publication_blockers'] ?? []))
|
|
->map(fn (mixed $blocker): string => $this->operatorBlockerLabel((string) $blocker))
|
|
->filter(static fn (string $blocker): bool => $blocker !== '')
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
$blockerCount = max((int) ($summary['publication_blocker_count'] ?? 0), count($blockers), count($missingReports));
|
|
$stepKey = $currentStep?->stepKeyEnum();
|
|
$stepStatus = $currentStep?->statusEnum();
|
|
$stepIsRunning = $stepStatus === ReviewPublicationResolutionStepStatus::Running;
|
|
$stepFailed = $stepStatus === ReviewPublicationResolutionStepStatus::Failed;
|
|
$readyToReturn = (string) $case->status === ReviewPublicationResolutionCaseStatus::ReadyToContinue->value
|
|
|| $stepKey === ReviewPublicationResolutionStepKey::ReturnToPublication;
|
|
$permissionNotice = $currentStep instanceof ReviewPublicationResolutionStep
|
|
&& $this->hasExecutableCurrentStep()
|
|
&& ! $stepIsRunning
|
|
&& ! $this->canExecuteCurrentStep()
|
|
? 'You can inspect this preparation flow, but you do not have permission to run the next action.'
|
|
: null;
|
|
$stateNotice = match (true) {
|
|
$stepIsRunning => [
|
|
'label' => 'Operation in progress',
|
|
'color' => 'info',
|
|
'body' => 'TenantPilot is waiting for the linked operation to finish. No new start action is available while it runs.',
|
|
],
|
|
$stepFailed => [
|
|
'label' => 'Action needed',
|
|
'color' => 'danger',
|
|
'body' => 'The last operation did not complete. Review the linked operation, then retry the current preparation action when you are ready.',
|
|
],
|
|
default => null,
|
|
};
|
|
|
|
return [
|
|
'headline' => $readyToReturn ? 'Review is ready to continue' : 'Review can\'t be published yet',
|
|
'status_badge_label' => match (true) {
|
|
$stepIsRunning => 'Operation in progress',
|
|
$stepFailed => 'Action needed',
|
|
$readyToReturn => 'Ready to continue',
|
|
default => 'Publication blocked',
|
|
},
|
|
'status_badge_color' => match (true) {
|
|
$stepIsRunning => 'info',
|
|
$stepFailed => 'danger',
|
|
$readyToReturn => 'success',
|
|
default => 'warning',
|
|
},
|
|
'blocked_summary' => $this->blockedSummary($blockerCount, count($missingReports)),
|
|
'blockers' => $blockers,
|
|
'missing_reports' => $missingReports,
|
|
'next_action_label' => $stepIsRunning ? 'Operation in progress' : $this->currentStepActionLabelFor($stepKey),
|
|
'next_action_description' => $this->nextStepDescription($stepKey, $stepStatus),
|
|
'after_this' => $this->afterNextStepDescription($stepKey),
|
|
'state_notice' => $stateNotice,
|
|
'permission_notice' => $permissionNotice,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function stepState(ReviewPublicationResolutionStep $step, ?ManagedEnvironment $tenant): array
|
|
{
|
|
$operationRun = $step->operationRun;
|
|
$stepKey = $step->stepKeyEnum();
|
|
|
|
return [
|
|
'key' => (string) $step->step_key,
|
|
'label' => (string) data_get($step->summary, 'label', $step->step_key),
|
|
'operator_label' => $this->operatorStepLabel($stepKey),
|
|
'description' => (string) data_get($step->summary, 'description', ''),
|
|
'operator_description' => $this->operatorStepDescription($stepKey),
|
|
'state_description' => (string) data_get($step->summary, 'state_description', ''),
|
|
'status' => (string) $step->status,
|
|
'status_label' => $this->statusLabel((string) $step->status),
|
|
'status_color' => $this->stepStatusColor((string) $step->status),
|
|
'proof_type' => $step->proof_type,
|
|
'proof_id' => $step->proof_id,
|
|
'proof_status' => $step->proof_status,
|
|
'proof_label' => (string) data_get($step->summary, 'proof_label', $this->proofLabelFromState($step)),
|
|
'proof_state_description' => (string) data_get($step->summary, 'proof_state_description', ''),
|
|
'proof_currentness' => (string) data_get($step->metadata, 'proof_currentness', ''),
|
|
'proof_usability' => (string) data_get($step->metadata, 'proof_usability', ''),
|
|
'proof_visibility' => (string) data_get($step->metadata, 'proof_visibility', ''),
|
|
'proof_reason_code' => (string) data_get($step->metadata, 'proof_reason_code', ''),
|
|
'proof_summary' => is_array(data_get($step->metadata, 'proof_summary')) ? data_get($step->metadata, 'proof_summary') : [],
|
|
'proof_url' => $this->proofUrl($step, $tenant),
|
|
'operation_run_id' => $operationRun instanceof OperationRun ? (int) $operationRun->getKey() : null,
|
|
'operation_url' => $this->operationUrl($step, $operationRun, $tenant),
|
|
];
|
|
}
|
|
|
|
private function operationUrl(
|
|
ReviewPublicationResolutionStep $step,
|
|
?OperationRun $operationRun,
|
|
?ManagedEnvironment $tenant,
|
|
): ?string {
|
|
if (! $this->canDiscloseOperationRun($step, $operationRun, $tenant)) {
|
|
return null;
|
|
}
|
|
|
|
return OperationRunLinks::view($operationRun, $tenant);
|
|
}
|
|
|
|
private function canDiscloseOperationRun(
|
|
ReviewPublicationResolutionStep $step,
|
|
?OperationRun $operationRun,
|
|
?ManagedEnvironment $tenant,
|
|
): bool {
|
|
if (! $operationRun instanceof OperationRun || ! $tenant instanceof ManagedEnvironment) {
|
|
return false;
|
|
}
|
|
|
|
if (! is_numeric($step->operation_run_id) || (int) $step->operation_run_id !== (int) $operationRun->getKey()) {
|
|
return false;
|
|
}
|
|
|
|
if ((int) $operationRun->workspace_id !== (int) $tenant->workspace_id
|
|
|| (int) $operationRun->managed_environment_id !== (int) $tenant->getKey()) {
|
|
return false;
|
|
}
|
|
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User || ! $user->can('view', $operationRun)) {
|
|
return false;
|
|
}
|
|
|
|
if ((string) data_get($step->metadata, 'proof_currentness') !== ResolutionProofCurrentness::Current->value) {
|
|
return false;
|
|
}
|
|
|
|
if ((string) data_get($step->metadata, 'proof_visibility') !== ResolutionProofVisibility::OperatorVisible->value) {
|
|
return false;
|
|
}
|
|
|
|
if (! in_array((string) data_get($step->metadata, 'proof_usability'), [
|
|
ResolutionProofUsability::Usable->value,
|
|
ResolutionProofUsability::UsableWithWarning->value,
|
|
ResolutionProofUsability::InspectionOnly->value,
|
|
], true)) {
|
|
return false;
|
|
}
|
|
|
|
$summary = data_get($step->metadata, 'proof_summary');
|
|
|
|
if (! is_array($summary)) {
|
|
return false;
|
|
}
|
|
|
|
return ResolutionProofEvaluation::sanitizeSummary($summary) === $summary;
|
|
}
|
|
|
|
private function proofUrl(ReviewPublicationResolutionStep $step, ?ManagedEnvironment $tenant): ?string
|
|
{
|
|
if (! $tenant instanceof ManagedEnvironment || ! is_numeric($step->proof_id) || ! is_string($step->proof_type)) {
|
|
return null;
|
|
}
|
|
|
|
return match ($step->proof_type) {
|
|
'environment_review' => $this->environmentReviewProofUrl($step, $tenant),
|
|
'stored_report' => $this->storedReportProofUrl($step, $tenant),
|
|
'evidence_snapshot' => $this->evidenceSnapshotProofUrl($step, $tenant),
|
|
'review_pack' => $this->reviewPackProofUrl($step, $tenant),
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
private function environmentReviewProofUrl(ReviewPublicationResolutionStep $step, ManagedEnvironment $tenant): ?string
|
|
{
|
|
$review = EnvironmentReview::query()->whereKey((int) $step->proof_id)->first();
|
|
|
|
if (! $review instanceof EnvironmentReview || ! EnvironmentReviewResource::canView($review)) {
|
|
return null;
|
|
}
|
|
|
|
return EnvironmentReviewResource::environmentScopedUrl('view', ['record' => (int) $step->proof_id], $tenant);
|
|
}
|
|
|
|
private function storedReportProofUrl(ReviewPublicationResolutionStep $step, ManagedEnvironment $tenant): ?string
|
|
{
|
|
$report = StoredReport::query()->whereKey((int) $step->proof_id)->first();
|
|
|
|
if (! $report instanceof StoredReport || ! StoredReportResource::canView($report)) {
|
|
return null;
|
|
}
|
|
|
|
return StoredReportResource::getUrl('view', ['record' => (int) $step->proof_id], tenant: $tenant);
|
|
}
|
|
|
|
private function evidenceSnapshotProofUrl(ReviewPublicationResolutionStep $step, ManagedEnvironment $tenant): ?string
|
|
{
|
|
$snapshot = EvidenceSnapshot::query()->whereKey((int) $step->proof_id)->first();
|
|
|
|
if (! $snapshot instanceof EvidenceSnapshot || ! EvidenceSnapshotResource::canView($snapshot)) {
|
|
return null;
|
|
}
|
|
|
|
return EvidenceSnapshotResource::getUrl('view', ['record' => (int) $step->proof_id], tenant: $tenant);
|
|
}
|
|
|
|
private function reviewPackProofUrl(ReviewPublicationResolutionStep $step, ManagedEnvironment $tenant): ?string
|
|
{
|
|
$reviewPack = ReviewPack::query()->whereKey((int) $step->proof_id)->first();
|
|
|
|
if (! $reviewPack instanceof ReviewPack || ! ReviewPackResource::canView($reviewPack)) {
|
|
return null;
|
|
}
|
|
|
|
return ReviewPackResource::getUrl('view', ['record' => (int) $step->proof_id], tenant: $tenant);
|
|
}
|
|
|
|
private function statusLabel(string $status): string
|
|
{
|
|
return str($status)->replace('_', ' ')->title()->toString();
|
|
}
|
|
|
|
private function caseStatusColor(string $status): string
|
|
{
|
|
return match ($status) {
|
|
ReviewPublicationResolutionCaseStatus::Completed->value => 'success',
|
|
ReviewPublicationResolutionCaseStatus::Blocked->value => 'danger',
|
|
ReviewPublicationResolutionCaseStatus::WaitingForRun->value => 'info',
|
|
ReviewPublicationResolutionCaseStatus::ReadyToContinue->value => 'warning',
|
|
ReviewPublicationResolutionCaseStatus::Cancelled->value,
|
|
ReviewPublicationResolutionCaseStatus::Superseded->value => 'gray',
|
|
default => 'primary',
|
|
};
|
|
}
|
|
|
|
private function stepStatusColor(string $status): string
|
|
{
|
|
return match ($status) {
|
|
ReviewPublicationResolutionStepStatus::Completed->value => 'success',
|
|
ReviewPublicationResolutionStepStatus::Failed->value => 'danger',
|
|
ReviewPublicationResolutionStepStatus::Running->value => 'info',
|
|
ReviewPublicationResolutionStepStatus::Actionable->value => 'warning',
|
|
ReviewPublicationResolutionStepStatus::Superseded->value => 'gray',
|
|
default => 'gray',
|
|
};
|
|
}
|
|
|
|
private function reportRequirementLabel(string $dimension): string
|
|
{
|
|
return match ($dimension) {
|
|
'permission_posture' => 'Permission posture',
|
|
'entra_admin_roles' => 'Entra admin roles',
|
|
default => str($dimension)->replace('_', ' ')->title()->toString(),
|
|
};
|
|
}
|
|
|
|
private function nextStepDescription(
|
|
?ReviewPublicationResolutionStepKey $stepKey,
|
|
?ReviewPublicationResolutionStepStatus $stepStatus = null,
|
|
): string {
|
|
if ($stepStatus === ReviewPublicationResolutionStepStatus::Running) {
|
|
return 'TenantPilot is waiting for the linked operation to finish. No new start action is available while it runs.';
|
|
}
|
|
|
|
if ($stepStatus === ReviewPublicationResolutionStepStatus::Failed) {
|
|
return 'The last operation did not complete. Review the linked operation, then retry the current preparation action when you are ready.';
|
|
}
|
|
|
|
return match ($stepKey) {
|
|
ReviewPublicationResolutionStepKey::CompleteRequiredReports => 'TenantPilot will update the missing required reports. It will not publish the review.',
|
|
ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot => 'This will collect a current evidence snapshot for the review. It will not publish the review.',
|
|
ReviewPublicationResolutionStepKey::RefreshReviewComposition => 'This will refresh the review from current evidence. It will not publish the review.',
|
|
ReviewPublicationResolutionStepKey::GenerateReviewPack => 'This will prepare the customer-ready export package. It will not publish the review.',
|
|
ReviewPublicationResolutionStepKey::ReturnToPublication => 'All checks are resolved. Return to the review and use the existing publish action when you are ready.',
|
|
default => 'TenantPilot will continue the next safe preparation step. It will not publish the review.',
|
|
};
|
|
}
|
|
|
|
private function afterNextStepDescription(?ReviewPublicationResolutionStepKey $stepKey): string
|
|
{
|
|
return match ($stepKey) {
|
|
ReviewPublicationResolutionStepKey::ReturnToPublication => 'Publishing remains a separate action on the review page.',
|
|
default => 'After this, TenantPilot re-checks the evidence, refreshes the review if needed, prepares the export if needed, and sends you back to the review. Publishing remains a separate action.',
|
|
};
|
|
}
|
|
|
|
private function currentStepConfirmationDescription(): string
|
|
{
|
|
return match ($this->currentStep()?->stepKeyEnum()) {
|
|
ReviewPublicationResolutionStepKey::CompleteRequiredReports => 'TenantPilot will update the missing required reports. This will not publish the review.',
|
|
ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot => 'TenantPilot will collect a current evidence snapshot for this review. This will not publish the review.',
|
|
ReviewPublicationResolutionStepKey::RefreshReviewComposition => 'TenantPilot will refresh the review from current evidence. This will not publish the review.',
|
|
ReviewPublicationResolutionStepKey::GenerateReviewPack => 'TenantPilot will prepare the customer-ready export package for this review. This will not publish the review.',
|
|
ReviewPublicationResolutionStepKey::ReturnToPublication => 'TenantPilot will return you to the review. Publishing remains a separate action.',
|
|
default => 'TenantPilot will continue the next safe preparation step. This will not publish the review.',
|
|
};
|
|
}
|
|
|
|
private function currentStepConfirmationHeading(): string
|
|
{
|
|
return $this->currentStepActionLabel().'?';
|
|
}
|
|
|
|
private function blockedSummary(int $blockerCount, int $missingReportCount): string
|
|
{
|
|
if ($missingReportCount > 0) {
|
|
return sprintf(
|
|
'TenantPilot found %d required %s that must be updated before this review can become customer-ready.',
|
|
$missingReportCount,
|
|
$missingReportCount === 1 ? 'report' : 'reports',
|
|
);
|
|
}
|
|
|
|
if ($blockerCount > 0) {
|
|
return sprintf(
|
|
'TenantPilot found %d missing %s before this review can become customer-ready.',
|
|
$blockerCount,
|
|
$blockerCount === 1 ? 'requirement' : 'requirements',
|
|
);
|
|
}
|
|
|
|
return 'TenantPilot is checking the remaining publication prerequisites before this review returns to the publish action.';
|
|
}
|
|
|
|
private function operatorBlockerLabel(string $blocker): string
|
|
{
|
|
$blocker = trim($blocker);
|
|
|
|
if ($blocker === '') {
|
|
return '';
|
|
}
|
|
|
|
if (str($blocker)->lower()->contains('report-backed evidence')) {
|
|
return 'Required reports are missing.';
|
|
}
|
|
|
|
return $blocker;
|
|
}
|
|
|
|
private function operatorStepLabel(?ReviewPublicationResolutionStepKey $stepKey): string
|
|
{
|
|
return match ($stepKey) {
|
|
ReviewPublicationResolutionStepKey::ValidateReviewReadiness => 'Check readiness',
|
|
ReviewPublicationResolutionStepKey::CompleteRequiredReports => 'Update required reports',
|
|
ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot => 'Collect evidence',
|
|
ReviewPublicationResolutionStepKey::RefreshReviewComposition => 'Refresh review',
|
|
ReviewPublicationResolutionStepKey::GenerateReviewPack => 'Prepare export',
|
|
ReviewPublicationResolutionStepKey::ReturnToPublication => 'Return to review',
|
|
default => 'Continue preparation',
|
|
};
|
|
}
|
|
|
|
private function operatorStepDescription(?ReviewPublicationResolutionStepKey $stepKey): string
|
|
{
|
|
return match ($stepKey) {
|
|
ReviewPublicationResolutionStepKey::ValidateReviewReadiness => 'TenantPilot checks whether the review is safe to prepare for publication.',
|
|
ReviewPublicationResolutionStepKey::CompleteRequiredReports => 'Required reports must be current before publication can continue.',
|
|
ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot => 'The review needs a complete and current evidence snapshot.',
|
|
ReviewPublicationResolutionStepKey::RefreshReviewComposition => 'The review is rebuilt from the latest evidence and report state.',
|
|
ReviewPublicationResolutionStepKey::GenerateReviewPack => 'The customer-ready export is prepared before returning to the review.',
|
|
ReviewPublicationResolutionStepKey::ReturnToPublication => 'Return to the review and decide whether to publish.',
|
|
default => 'TenantPilot prepares the next safe publication prerequisite.',
|
|
};
|
|
}
|
|
|
|
private function proofLabelFromState(ReviewPublicationResolutionStep $step): string
|
|
{
|
|
return match ((string) $step->status) {
|
|
ReviewPublicationResolutionStepStatus::Running->value => 'Operation running',
|
|
ReviewPublicationResolutionStepStatus::Failed->value => 'Action failed',
|
|
ReviewPublicationResolutionStepStatus::Completed->value => 'Current proof',
|
|
default => is_string($step->proof_type) ? 'Proof cannot be verified' : 'Proof missing',
|
|
};
|
|
}
|
|
}
|