922 lines
34 KiB
PHP
922 lines
34 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\Pages\Monitoring;
|
|
|
|
use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState;
|
|
use App\Filament\Resources\OperationRunResource;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\OperationRun;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
|
|
use App\Services\Auth\WorkspaceCapabilityResolver;
|
|
use App\Support\Filament\CanonicalAdminEnvironmentFilterState;
|
|
use App\Support\ManagedEnvironmentLinks;
|
|
use App\Support\Navigation\CanonicalNavigationContext;
|
|
use App\Support\Navigation\WorkspaceHubEnvironmentFilter;
|
|
use App\Support\OperateHub\OperateHubShell;
|
|
use App\Support\OperationCatalog;
|
|
use App\Support\OperationRunLinks;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\Operations\OperationLifecyclePolicy;
|
|
use App\Support\OpsUx\OperationRunProgressContract;
|
|
use App\Support\OpsUx\OperationUxPresenter;
|
|
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\Workspaces\WorkspaceContext;
|
|
use BackedEnum;
|
|
use Filament\Actions\Action;
|
|
use Filament\Facades\Filament;
|
|
use Filament\Forms\Concerns\InteractsWithForms;
|
|
use Filament\Forms\Contracts\HasForms;
|
|
use Filament\Pages\Page;
|
|
use Filament\Tables\Concerns\InteractsWithTable;
|
|
use Filament\Tables\Contracts\HasTable;
|
|
use Filament\Tables\Table;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Str;
|
|
use UnitEnum;
|
|
|
|
class Operations extends Page implements HasForms, HasTable
|
|
{
|
|
use ClearsWorkspaceHubEnvironmentFilterState;
|
|
use InteractsWithForms;
|
|
use InteractsWithTable;
|
|
|
|
protected const MONITORING_PAGE_STATE_CONTRACT = [
|
|
'surfaceKey' => 'operations',
|
|
'surfaceType' => 'simple_monitoring',
|
|
'stateFields' => [
|
|
[
|
|
'stateKey' => 'environment_id',
|
|
'stateClass' => 'contextual_prefilter',
|
|
'carrier' => 'query_param',
|
|
'queryRole' => 'durable_restorable',
|
|
'shareable' => true,
|
|
'restorableOnRefresh' => true,
|
|
'tenantSensitive' => true,
|
|
'invalidFallback' => 'discard_and_continue',
|
|
],
|
|
[
|
|
'stateKey' => 'problemClass',
|
|
'stateClass' => 'contextual_prefilter',
|
|
'carrier' => 'query_param',
|
|
'queryRole' => 'scoped_deeplink',
|
|
'shareable' => true,
|
|
'restorableOnRefresh' => true,
|
|
'tenantSensitive' => false,
|
|
'invalidFallback' => 'discard_and_continue',
|
|
],
|
|
[
|
|
'stateKey' => 'activeTab',
|
|
'stateClass' => 'active',
|
|
'carrier' => 'livewire_property',
|
|
'queryRole' => 'durable_restorable',
|
|
'shareable' => true,
|
|
'restorableOnRefresh' => true,
|
|
'tenantSensitive' => false,
|
|
'invalidFallback' => 'discard_and_continue',
|
|
],
|
|
[
|
|
'stateKey' => 'tableFilters',
|
|
'stateClass' => 'shareable_restorable',
|
|
'carrier' => 'session',
|
|
'queryRole' => 'unsupported',
|
|
'shareable' => false,
|
|
'restorableOnRefresh' => true,
|
|
'tenantSensitive' => true,
|
|
'invalidFallback' => 'discard_and_continue',
|
|
],
|
|
],
|
|
'hydrationRule' => [
|
|
'precedenceOrder' => ['query', 'session', 'default'],
|
|
'appliesOnInitialMountOnly' => true,
|
|
'activeStateBecomesAuthoritativeAfterMount' => true,
|
|
'clearsOnTenantSwitch' => ['environment_id', 'managed_environment_id', 'type', 'initiator_name'],
|
|
'invalidRequestedStateFallback' => 'discard_and_continue',
|
|
],
|
|
'inspectContract' => [
|
|
'primaryModel' => 'none',
|
|
'selectedStateKey' => null,
|
|
'openedBy' => [],
|
|
'presentation' => 'none',
|
|
'shareable' => false,
|
|
'invalidSelectionFallback' => 'discard_and_continue',
|
|
],
|
|
'shareableStateKeys' => ['environment_id', 'problemClass', 'activeTab'],
|
|
'localOnlyStateKeys' => [],
|
|
];
|
|
|
|
public string $activeTab = 'all';
|
|
|
|
/**
|
|
* @var array<string, mixed>|null
|
|
*/
|
|
public ?array $navigationContextPayload = null;
|
|
|
|
protected static bool $isDiscovered = false;
|
|
|
|
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-queue-list';
|
|
|
|
protected static string|UnitEnum|null $navigationGroup = 'Monitoring';
|
|
|
|
protected static ?string $title = 'Operations';
|
|
|
|
// Must be non-static
|
|
protected string $view = 'filament.pages.monitoring.operations';
|
|
|
|
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
|
{
|
|
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::RunLog, ActionSurfaceType::ReadOnlyRegistryReport)
|
|
->withDefaults(new ActionSurfaceDefaults(
|
|
moreGroupLabel: 'More',
|
|
exportIsDefaultBulkActionForReadOnly: false,
|
|
))
|
|
->satisfy(ActionSurfaceSlot::ListHeader, 'Header actions preserve scope context and return navigation for the monitoring operations list.')
|
|
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value)
|
|
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'Operation runs remain immutable on the monitoring list and intentionally omit bulk actions.')
|
|
->satisfy(ActionSurfaceSlot::ListEmptyState, 'The empty state explains when no operation runs exist for the active workspace scope.')
|
|
->exempt(ActionSurfaceSlot::DetailHeader, 'Row navigation opens the canonical tenantless operation detail page, which owns header actions.');
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function mount(): void
|
|
{
|
|
$user = auth()->user();
|
|
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
|
|
|
|
if (! $user instanceof User || ! is_int($workspaceId)) {
|
|
abort(404);
|
|
}
|
|
|
|
$workspace = Workspace::query()->whereKey($workspaceId)->first();
|
|
|
|
if (! $workspace instanceof Workspace) {
|
|
abort(404);
|
|
}
|
|
|
|
/** @var WorkspaceCapabilityResolver $resolver */
|
|
$resolver = app(WorkspaceCapabilityResolver::class);
|
|
|
|
if (! $resolver->isMember($user, $workspace)) {
|
|
abort(404);
|
|
}
|
|
|
|
$this->navigationContextPayload = is_array(request()->query('nav')) ? request()->query('nav') : null;
|
|
|
|
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
|
|
|
|
$this->mountInteractsWithTable();
|
|
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
|
|
$this->applyRequestedDashboardPrefilter();
|
|
}
|
|
|
|
protected function getHeaderWidgets(): array
|
|
{
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @return array<Action>
|
|
*/
|
|
protected function getHeaderActions(): array
|
|
{
|
|
$operateHubShell = app(OperateHubShell::class);
|
|
$navigationContext = $this->navigationContext();
|
|
|
|
$activeEnvironment = $this->currentTenantFilterId() === null
|
|
? $operateHubShell->activeEntitledTenant(request())
|
|
: null;
|
|
|
|
$actions = [];
|
|
|
|
if ($activeEnvironment instanceof ManagedEnvironment) {
|
|
$actions[] = Action::make('operate_hub_scope_operations')
|
|
->label($operateHubShell->scopeLabel(request()))
|
|
->color('gray')
|
|
->disabled();
|
|
}
|
|
|
|
if ($navigationContext?->backLinkLabel !== null && $navigationContext->backLinkUrl !== null) {
|
|
$actions[] = Action::make('operate_hub_back_to_origin_operations')
|
|
->label($navigationContext->backLinkLabel)
|
|
->icon('heroicon-o-arrow-left')
|
|
->color('gray')
|
|
->url($navigationContext->backLinkUrl);
|
|
} elseif ($activeEnvironment instanceof ManagedEnvironment) {
|
|
$actions[] = Action::make('operate_hub_back_to_tenant_operations')
|
|
->label('Back to '.$activeEnvironment->name)
|
|
->icon('heroicon-o-arrow-left')
|
|
->color('gray')
|
|
->url(ManagedEnvironmentLinks::viewUrl($activeEnvironment));
|
|
}
|
|
|
|
if ($activeEnvironment instanceof ManagedEnvironment) {
|
|
$actions[] = Action::make('operate_hub_show_all_tenants')
|
|
->label(__('localization.shell.show_all_environments'))
|
|
->color('gray')
|
|
->action(function (): void {
|
|
Filament::setTenant(null, true);
|
|
|
|
app(WorkspaceContext::class)->clearLastEnvironmentId(request());
|
|
|
|
$this->removeTableFilter('managed_environment_id');
|
|
|
|
$this->redirect(OperationRunLinks::index(allTenants: true));
|
|
});
|
|
}
|
|
|
|
return $actions;
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* scope_label: string,
|
|
* scope_body: string,
|
|
* return_label: ?string,
|
|
* return_body: ?string,
|
|
* scope_reset_label: ?string,
|
|
* scope_reset_body: ?string,
|
|
* inspect_body: string
|
|
* }
|
|
*/
|
|
public function landingHierarchySummary(): array
|
|
{
|
|
$operateHubShell = app(OperateHubShell::class);
|
|
$navigationContext = $this->navigationContext();
|
|
$filteredTenant = $this->filteredTenant();
|
|
$activeEnvironment = $filteredTenant instanceof ManagedEnvironment
|
|
? null
|
|
: $operateHubShell->activeEntitledTenant(request());
|
|
|
|
$returnLabel = null;
|
|
$returnBody = null;
|
|
|
|
if ($navigationContext?->backLinkLabel !== null && $navigationContext->backLinkUrl !== null) {
|
|
$returnLabel = $navigationContext->backLinkLabel;
|
|
$returnBody = 'Return to the originating monitoring surface without competing with the current tab, filters, or row inspection flow.';
|
|
} elseif ($activeEnvironment instanceof ManagedEnvironment) {
|
|
$returnLabel = 'Back to '.$activeEnvironment->name;
|
|
$returnBody = 'Return to the environment dashboard when you need environment-specific context outside this workspace operations view.';
|
|
}
|
|
|
|
return [
|
|
'scope_label' => $operateHubShell->scopeLabel(request()),
|
|
'scope_body' => $filteredTenant instanceof ManagedEnvironment
|
|
? 'Filtered to one environment in this workspace.'
|
|
: ($activeEnvironment instanceof ManagedEnvironment
|
|
? 'Showing the active environment inside this workspace.'
|
|
: 'Showing workspace-wide execution records across entitled environments.'),
|
|
'return_label' => $returnLabel,
|
|
'return_body' => $returnBody,
|
|
'scope_reset_label' => $activeEnvironment instanceof ManagedEnvironment ? __('localization.shell.show_all_environments') : null,
|
|
'scope_reset_body' => $activeEnvironment instanceof ManagedEnvironment
|
|
? 'Reset Operations Hub back to workspace-wide execution records when environment-specific context is no longer needed.'
|
|
: null,
|
|
'inspect_body' => 'Open a run from the table to enter the canonical monitoring detail viewer.',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* question:string,
|
|
* has_attention:bool,
|
|
* selected_operation:array<string,mixed>|null,
|
|
* diagnostics:array{label:string,state:string,body:string}
|
|
* }
|
|
*/
|
|
public function decisionWorkbench(): array
|
|
{
|
|
$selectedOperation = $this->selectedWorkbenchOperation();
|
|
$needsAttention = $this->summaryCount(
|
|
fn (Builder $query): Builder => $query->dashboardNeedsFollowUp(),
|
|
);
|
|
|
|
return [
|
|
'question' => 'Which operation needs attention now?',
|
|
'has_attention' => $needsAttention > 0,
|
|
'selected_operation' => $selectedOperation instanceof OperationRun
|
|
? $this->workbenchOperationPayload($selectedOperation, $needsAttention > 0)
|
|
: null,
|
|
'diagnostics' => [
|
|
'label' => 'Diagnostics',
|
|
'state' => 'Collapsed',
|
|
'body' => 'Raw context, provider payloads, stack traces, debug metadata, and support diagnostics stay on authorized operation detail surfaces.',
|
|
],
|
|
];
|
|
}
|
|
|
|
private function selectedWorkbenchOperation(): ?OperationRun
|
|
{
|
|
$attentionRun = $this->topOperationFromQuery(
|
|
fn (Builder $query): Builder => $query->dashboardNeedsFollowUp(),
|
|
sortByAttention: true,
|
|
);
|
|
|
|
if ($attentionRun instanceof OperationRun) {
|
|
return $attentionRun;
|
|
}
|
|
|
|
$activeRun = $this->topOperationFromQuery(
|
|
fn (Builder $query): Builder => $query->active(),
|
|
);
|
|
|
|
if ($activeRun instanceof OperationRun) {
|
|
return $activeRun;
|
|
}
|
|
|
|
return $this->topOperationFromQuery();
|
|
}
|
|
|
|
private function topOperationFromQuery(?callable $scope = null, bool $sortByAttention = false): ?OperationRun
|
|
{
|
|
$query = $this->scopedSummaryQuery();
|
|
|
|
if (! $query instanceof Builder) {
|
|
return null;
|
|
}
|
|
|
|
$query
|
|
->with('tenant')
|
|
->latest('id')
|
|
->limit(50);
|
|
|
|
if ($scope !== null) {
|
|
$query = $scope($query);
|
|
}
|
|
|
|
/** @var Collection<int, OperationRun> $runs */
|
|
$runs = $query->get();
|
|
|
|
if ($runs->isEmpty()) {
|
|
return null;
|
|
}
|
|
|
|
if (! $sortByAttention) {
|
|
return $runs->first();
|
|
}
|
|
|
|
return $runs
|
|
->sort(function (OperationRun $left, OperationRun $right): int {
|
|
return [
|
|
$this->attentionPriority($right),
|
|
$right->created_at?->getTimestamp() ?? 0,
|
|
(int) $right->getKey(),
|
|
] <=> [
|
|
$this->attentionPriority($left),
|
|
$left->created_at?->getTimestamp() ?? 0,
|
|
(int) $left->getKey(),
|
|
];
|
|
})
|
|
->first();
|
|
}
|
|
|
|
private function attentionPriority(OperationRun $run): int
|
|
{
|
|
if ((string) $run->outcome === OperationRunOutcome::Blocked->value) {
|
|
return 50;
|
|
}
|
|
|
|
if ((string) $run->outcome === OperationRunOutcome::Failed->value) {
|
|
return 40;
|
|
}
|
|
|
|
if ((string) $run->outcome === OperationRunOutcome::PartiallySucceeded->value) {
|
|
return 30;
|
|
}
|
|
|
|
if ($run->problemClass() === OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION) {
|
|
return 20;
|
|
}
|
|
|
|
if ($run->problemClass() === OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP) {
|
|
return 10;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function workbenchOperationPayload(OperationRun $run, bool $hasAttention): array
|
|
{
|
|
$progress = OperationRunProgressContract::forRun($run);
|
|
$decisionTruth = OperationUxPresenter::decisionZoneTruth($run);
|
|
$actionDecision = OperationRunResource::actionDecision($run);
|
|
$primaryAction = is_array($actionDecision['primary_action'] ?? null) ? $actionDecision['primary_action'] : null;
|
|
$tenant = $run->tenant;
|
|
|
|
return [
|
|
'id' => (int) $run->getKey(),
|
|
'title' => OperationCatalog::label((string) $run->type),
|
|
'identifier' => OperationRunLinks::identifier($run),
|
|
'status_label' => $this->humanizeState((string) $run->status),
|
|
'outcome_label' => $this->humanizeState((string) $run->outcome),
|
|
'attention_label' => $hasAttention && $run->requiresOperatorReview()
|
|
? 'Needs attention'
|
|
: ($run->isCurrentlyActive() ? 'Active operation' : 'No attention needed'),
|
|
'reason' => $this->operationReason($run, $decisionTruth),
|
|
'impact' => $this->operationImpact($run),
|
|
'environment' => $tenant instanceof ManagedEnvironment ? (string) $tenant->name : 'Workspace-level operation',
|
|
'timing' => $this->operationTiming($run),
|
|
'proof_label' => 'Operation detail available',
|
|
'proof_body' => (string) ($actionDecision['attention_reason'] ?? 'Open operation for stored proof, related links, and authorized diagnostics.'),
|
|
'primary_action_label' => is_string($primaryAction['label'] ?? null)
|
|
? (string) $primaryAction['label']
|
|
: OperationRunLinks::openLabel(),
|
|
'primary_action_url' => OperationRunResource::primaryActionUrl($run),
|
|
'progress' => $progress,
|
|
'progress_label' => is_string($progress['label'] ?? null) ? $progress['label'] : null,
|
|
'show_progress_bar' => ($progress['display'] ?? null) === OperationRunProgressContract::COUNTED,
|
|
'progress_percent' => is_int($progress['percent'] ?? null) ? $progress['percent'] : null,
|
|
'outcome_guidance' => OperationUxPresenter::surfaceGuidance($run) ?? 'Review the operation detail for the next safe step.',
|
|
'diagnostics_available' => ! empty($run->failure_summary) || ! empty($run->context),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $decisionTruth
|
|
*/
|
|
private function operationReason(OperationRun $run, array $decisionTruth): string
|
|
{
|
|
$attentionNote = $decisionTruth['attentionNote'] ?? null;
|
|
|
|
if (is_string($attentionNote) && trim($attentionNote) !== '') {
|
|
return $attentionNote;
|
|
}
|
|
|
|
$lifecycleLabel = $decisionTruth['freshnessLabel'] ?? null;
|
|
|
|
if (is_string($lifecycleLabel) && trim($lifecycleLabel) !== '') {
|
|
return $lifecycleLabel;
|
|
}
|
|
|
|
return match ((string) $run->outcome) {
|
|
OperationRunOutcome::Blocked->value => 'The operation is blocked by an execution prerequisite recorded on the run.',
|
|
OperationRunOutcome::Failed->value => 'The operation finished with a failed outcome and needs review before retrying or relying on the result.',
|
|
OperationRunOutcome::PartiallySucceeded->value => 'The operation finished partially and needs follow-up on affected items.',
|
|
OperationRunOutcome::Succeeded->value => 'The operation completed successfully. This is execution truth only, not an environment health claim.',
|
|
default => $run->isCurrentlyActive()
|
|
? 'The operation is still active. Progress is shown only from trusted run counts.'
|
|
: 'Reason unavailable from stored operation truth.',
|
|
};
|
|
}
|
|
|
|
private function operationImpact(OperationRun $run): string
|
|
{
|
|
if ($run->problemClass() === OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION) {
|
|
return 'Follow-up is needed before starting overlapping work for this operation scope.';
|
|
}
|
|
|
|
return match ((string) $run->outcome) {
|
|
OperationRunOutcome::Blocked->value => 'Execution did not proceed; inspect the blocked prerequisite before retrying from the source surface.',
|
|
OperationRunOutcome::Failed->value => 'The expected result may be incomplete or unavailable until the failure is reviewed.',
|
|
OperationRunOutcome::PartiallySucceeded->value => 'Some work completed, but affected items may still require review or a targeted rerun.',
|
|
OperationRunOutcome::Succeeded->value => 'Execution completed; use operation detail for proof rather than treating this as governance health.',
|
|
default => $run->isCurrentlyActive()
|
|
? 'Work is in progress; avoid duplicate starts until this run settles or becomes stale.'
|
|
: 'Impact unavailable from stored operation truth.',
|
|
};
|
|
}
|
|
|
|
private function operationTiming(OperationRun $run): string
|
|
{
|
|
if ($run->completed_at !== null) {
|
|
return 'Completed '.$run->completed_at->diffForHumans();
|
|
}
|
|
|
|
if ($run->started_at !== null) {
|
|
return 'Started '.$run->started_at->diffForHumans();
|
|
}
|
|
|
|
if ($run->created_at !== null) {
|
|
return 'Created '.$run->created_at->diffForHumans();
|
|
}
|
|
|
|
return 'Timing unavailable';
|
|
}
|
|
|
|
private function summaryCount(callable $scope): int
|
|
{
|
|
$query = $this->scopedSummaryQuery();
|
|
|
|
if (! $query instanceof Builder) {
|
|
return 0;
|
|
}
|
|
|
|
return (int) $scope($query)->count();
|
|
}
|
|
|
|
private function humanizeState(string $state): string
|
|
{
|
|
$label = OperationRunOutcome::uiLabels(includeReserved: true)[$state] ?? null;
|
|
|
|
if (is_string($label)) {
|
|
return $label;
|
|
}
|
|
|
|
return Str::of($state)->replace('_', ' ')->headline()->toString();
|
|
}
|
|
|
|
public function tabUrl(string $tab): string
|
|
{
|
|
$normalizedTab = in_array($tab, self::supportedTabs(), true) ? $tab : 'all';
|
|
|
|
return $this->operationsUrl([
|
|
'activeTab' => $normalizedTab !== 'all' ? $normalizedTab : null,
|
|
'problemClass' => in_array($normalizedTab, self::problemClassTabs(), true) ? $normalizedTab : null,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @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(route('admin.operations.index', [
|
|
'workspace' => app(WorkspaceContext::class)->currentWorkspace(request()),
|
|
])),
|
|
];
|
|
}
|
|
|
|
private function navigationContext(): ?CanonicalNavigationContext
|
|
{
|
|
if (! is_array($this->navigationContextPayload)) {
|
|
return CanonicalNavigationContext::fromRequest(request());
|
|
}
|
|
|
|
return CanonicalNavigationContext::fromPayload($this->navigationContextPayload);
|
|
}
|
|
|
|
public function updatedActiveTab(): void
|
|
{
|
|
$this->resetPage();
|
|
}
|
|
|
|
public function table(Table $table): Table
|
|
{
|
|
return OperationRunResource::table($table)
|
|
->query(function (): Builder {
|
|
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
|
|
$tenantFilter = $this->currentTenantFilterId();
|
|
$allowedTenantIds = $this->allowedTenantIdsForWorkspaceScope($workspaceId);
|
|
|
|
$query = OperationRun::query()
|
|
->with(['tenant', 'user'])
|
|
->latest('id')
|
|
->when(
|
|
$workspaceId,
|
|
fn (Builder $query): Builder => $query->where('workspace_id', (int) $workspaceId),
|
|
)
|
|
->when(
|
|
! $workspaceId,
|
|
fn (Builder $query): Builder => $query->whereRaw('1 = 0'),
|
|
)
|
|
->when(
|
|
$workspaceId && $allowedTenantIds !== null,
|
|
function (Builder $query) use ($allowedTenantIds): Builder {
|
|
return $query->where(function (Builder $query) use ($allowedTenantIds): void {
|
|
$query->whereNull('managed_environment_id');
|
|
|
|
if ($allowedTenantIds !== []) {
|
|
$query->orWhereIn('managed_environment_id', $allowedTenantIds);
|
|
}
|
|
});
|
|
},
|
|
)
|
|
->when(
|
|
$tenantFilter !== null,
|
|
fn (Builder $query): Builder => $query->where('managed_environment_id', $tenantFilter),
|
|
);
|
|
|
|
return $this->applyActiveTab($query);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @return array{likely_stale:int,reconciled:int}
|
|
*/
|
|
public function lifecycleVisibilitySummary(): array
|
|
{
|
|
$baseQuery = $this->scopedSummaryQuery();
|
|
|
|
if (! $baseQuery instanceof Builder) {
|
|
return [
|
|
'likely_stale' => 0,
|
|
'reconciled' => 0,
|
|
];
|
|
}
|
|
|
|
$reconciled = (clone $baseQuery)
|
|
->whereNotNull('context->reconciliation->reconciled_at')
|
|
->count();
|
|
|
|
$policy = app(OperationLifecyclePolicy::class);
|
|
$likelyStale = (clone $baseQuery)
|
|
->likelyStale($policy)
|
|
->count();
|
|
|
|
return [
|
|
'likely_stale' => $likelyStale,
|
|
'reconciled' => $reconciled,
|
|
];
|
|
}
|
|
|
|
private function applyActiveTab(Builder $query): Builder
|
|
{
|
|
return match ($this->activeTab) {
|
|
'active' => $query->healthyActive(),
|
|
OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION => $query->activeStaleAttention(),
|
|
OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP => $query->terminalFollowUp(),
|
|
'blocked' => $query->dashboardNeedsFollowUp(),
|
|
'succeeded' => $query
|
|
->where('status', OperationRunStatus::Completed->value)
|
|
->where('outcome', OperationRunOutcome::Succeeded->value),
|
|
'partial' => $query
|
|
->where('status', OperationRunStatus::Completed->value)
|
|
->where('outcome', OperationRunOutcome::PartiallySucceeded->value),
|
|
'failed' => $query
|
|
->where('status', OperationRunStatus::Completed->value)
|
|
->where('outcome', OperationRunOutcome::Failed->value),
|
|
default => $query,
|
|
};
|
|
}
|
|
|
|
private function scopedSummaryQuery(): ?Builder
|
|
{
|
|
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
|
|
|
|
if (! $workspaceId) {
|
|
return null;
|
|
}
|
|
|
|
$tenantFilter = $this->currentTenantFilterId();
|
|
$allowedTenantIds = $this->allowedTenantIdsForWorkspaceScope($workspaceId);
|
|
|
|
return OperationRun::query()
|
|
->where('workspace_id', (int) $workspaceId)
|
|
->when(
|
|
$allowedTenantIds !== null,
|
|
function (Builder $query) use ($allowedTenantIds): Builder {
|
|
return $query->where(function (Builder $query) use ($allowedTenantIds): void {
|
|
$query->whereNull('managed_environment_id');
|
|
|
|
if ($allowedTenantIds !== []) {
|
|
$query->orWhereIn('managed_environment_id', $allowedTenantIds);
|
|
}
|
|
});
|
|
},
|
|
)
|
|
->when(
|
|
$tenantFilter !== null,
|
|
fn (Builder $query): Builder => $query->where('managed_environment_id', $tenantFilter),
|
|
);
|
|
}
|
|
|
|
private function applyRequestedDashboardPrefilter(): void
|
|
{
|
|
$workspace = app(WorkspaceContext::class)->currentWorkspace(request());
|
|
|
|
if ($workspace instanceof Workspace) {
|
|
$filter = WorkspaceHubEnvironmentFilter::fromRequest(request(), $workspace);
|
|
|
|
if ($filter instanceof WorkspaceHubEnvironmentFilter) {
|
|
$requestedTenantId = $this->normalizeEntitledTenantFilter($filter->environmentId());
|
|
|
|
if ($requestedTenantId === null) {
|
|
abort(404);
|
|
}
|
|
|
|
$tenantId = (string) $requestedTenantId;
|
|
$this->tableFilters['managed_environment_id']['value'] = $tenantId;
|
|
$this->tableDeferredFilters['managed_environment_id']['value'] = $tenantId;
|
|
}
|
|
}
|
|
|
|
$requestedOperationType = request()->query('operation_type');
|
|
|
|
if (is_string($requestedOperationType) && trim($requestedOperationType) !== '') {
|
|
$canonicalOperationType = OperationCatalog::canonicalCode($requestedOperationType);
|
|
|
|
if (OperationCatalog::rawValuesForCanonical($canonicalOperationType) !== []) {
|
|
$this->tableFilters['type']['value'] = $canonicalOperationType;
|
|
$this->tableDeferredFilters['type']['value'] = $canonicalOperationType;
|
|
}
|
|
}
|
|
|
|
$requestedProblemClass = request()->query('problemClass');
|
|
|
|
if (in_array($requestedProblemClass, self::problemClassTabs(), true)) {
|
|
$this->activeTab = (string) $requestedProblemClass;
|
|
|
|
return;
|
|
}
|
|
|
|
$requestedTab = request()->query('activeTab');
|
|
|
|
if (in_array($requestedTab, self::supportedTabs(), true)) {
|
|
$this->activeTab = (string) $requestedTab;
|
|
}
|
|
}
|
|
|
|
private function operationsUrl(array $overrides = []): string
|
|
{
|
|
$parameters = array_merge(
|
|
['workspace' => app(WorkspaceContext::class)->currentWorkspace(request())],
|
|
$this->navigationContext()?->toQuery() ?? [],
|
|
[
|
|
'environment_id' => $this->currentTenantFilterId(),
|
|
'activeTab' => $this->activeTab !== 'all' ? $this->activeTab : null,
|
|
'problemClass' => in_array($this->activeTab, self::problemClassTabs(), true) ? $this->activeTab : null,
|
|
],
|
|
$overrides,
|
|
);
|
|
|
|
return route(
|
|
'admin.operations.index',
|
|
array_filter($parameters, static fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []),
|
|
);
|
|
}
|
|
|
|
private function currentTenantFilterId(): ?int
|
|
{
|
|
$tenantFilter = app(CanonicalAdminEnvironmentFilterState::class)->currentFilterValue(
|
|
$this->getTableFiltersSessionKey(),
|
|
$this->tableFilters ?? [],
|
|
request(),
|
|
);
|
|
|
|
return $this->normalizeEntitledTenantFilter($tenantFilter);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Null means inherited access to all environments in the workspace.
|
|
*
|
|
* @return list<int>|null
|
|
*/
|
|
private function allowedTenantIdsForWorkspaceScope(mixed $workspaceId): ?array
|
|
{
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User || ! is_int($workspaceId)) {
|
|
return [];
|
|
}
|
|
|
|
$allowedIds = app(ManagedEnvironmentAccessScopeResolver::class)
|
|
->allowedManagedEnvironmentIdsForWorkspace($user, $workspaceId);
|
|
|
|
if ($allowedIds === null) {
|
|
return null;
|
|
}
|
|
|
|
return array_values(array_unique(array_map('intval', $allowedIds)));
|
|
}
|
|
|
|
private function normalizeEntitledTenantFilter(mixed $value): ?int
|
|
{
|
|
if (! is_numeric($value)) {
|
|
return null;
|
|
}
|
|
|
|
$tenantId = (int) $value;
|
|
|
|
return in_array($tenantId, $this->authorizedTenantIds(), true)
|
|
? $tenantId
|
|
: null;
|
|
}
|
|
|
|
/**
|
|
* @return list<int>
|
|
*/
|
|
private function authorizedTenantIds(): array
|
|
{
|
|
$user = auth()->user();
|
|
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
|
|
|
|
if (! $user instanceof User || ! is_int($workspaceId)) {
|
|
return [];
|
|
}
|
|
|
|
return collect($user->getTenants(Filament::getCurrentOrDefaultPanel()))
|
|
->filter(static fn (ManagedEnvironment $tenant): bool => (int) $tenant->workspace_id === $workspaceId)
|
|
->filter(static fn (ManagedEnvironment $tenant): bool => $tenant->isActive())
|
|
->map(static fn (ManagedEnvironment $tenant): int => (int) $tenant->getKey())
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @return array<int, ManagedEnvironment>
|
|
*/
|
|
private function authorizedTenants(): array
|
|
{
|
|
$user = auth()->user();
|
|
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
|
|
|
|
if (! $user instanceof User || ! is_int($workspaceId)) {
|
|
return [];
|
|
}
|
|
|
|
return collect($user->getTenants(Filament::getCurrentOrDefaultPanel()))
|
|
->filter(static fn (ManagedEnvironment $tenant): bool => (int) $tenant->workspace_id === $workspaceId)
|
|
->filter(static fn (ManagedEnvironment $tenant): bool => $tenant->isActive())
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
private static function supportedTabs(): array
|
|
{
|
|
return [
|
|
'all',
|
|
'active',
|
|
'blocked',
|
|
'succeeded',
|
|
'partial',
|
|
'failed',
|
|
...self::problemClassTabs(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
private static function problemClassTabs(): array
|
|
{
|
|
return [
|
|
OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION,
|
|
OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP,
|
|
];
|
|
}
|
|
}
|