## Summary - productize the operations hub decision-first workbench and related monitoring page surfaces - add the operations workbench stats widget plus tenantless run viewer and admin scope updates - extend monitoring, ops UX, and browser coverage for the new workbench behavior - add Spec 328 artifacts under `specs/328-operations-hub-decision-first-workbench-productization` ## Testing - not run as part of this handoff Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #389
159 lines
5.6 KiB
PHP
159 lines
5.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\Widgets\Operations;
|
|
|
|
use App\Models\OperationRun;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
|
|
use App\Support\Navigation\WorkspaceHubEnvironmentFilter;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Filament\Support\Enums\IconPosition;
|
|
use Filament\Widgets\StatsOverviewWidget;
|
|
use Filament\Widgets\StatsOverviewWidget\Stat;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
|
|
class OperationsWorkbenchStats extends StatsOverviewWidget
|
|
{
|
|
protected static bool $isLazy = false;
|
|
|
|
protected int|string|array $columnSpan = 'full';
|
|
|
|
protected int|array|null $columns = [
|
|
'@xl' => 4,
|
|
'!@lg' => 4,
|
|
];
|
|
|
|
protected ?string $pollingInterval = null;
|
|
|
|
/**
|
|
* @return array<Stat>
|
|
*/
|
|
protected function getStats(): array
|
|
{
|
|
$needsAttention = $this->summaryCount(
|
|
fn (Builder $query): Builder => $query->dashboardNeedsFollowUp(),
|
|
);
|
|
$activeOperations = $this->summaryCount(
|
|
fn (Builder $query): Builder => $query->active(),
|
|
);
|
|
$failedOrBlocked = $this->summaryCount(fn (Builder $query): Builder => $query
|
|
->where('status', OperationRunStatus::Completed->value)
|
|
->whereIn('outcome', [
|
|
OperationRunOutcome::Failed->value,
|
|
OperationRunOutcome::Blocked->value,
|
|
]));
|
|
$completedRecently = $this->summaryCount(fn (Builder $query): Builder => $query
|
|
->where('status', OperationRunStatus::Completed->value)
|
|
->where('completed_at', '>=', now()->subDay()));
|
|
|
|
return [
|
|
$this->workbenchStat(
|
|
key: 'needs-attention',
|
|
label: 'Needs attention',
|
|
value: $needsAttention,
|
|
description: 'Failed, blocked, partial, or stale OperationRuns in scope.',
|
|
color: $needsAttention > 0 ? 'warning' : 'gray',
|
|
descriptionIcon: 'heroicon-m-exclamation-triangle',
|
|
),
|
|
$this->workbenchStat(
|
|
key: 'active-operations',
|
|
label: 'Active operations',
|
|
value: $activeOperations,
|
|
description: 'Queued or running records with trusted progress only.',
|
|
color: 'info',
|
|
descriptionIcon: 'heroicon-m-bolt',
|
|
),
|
|
$this->workbenchStat(
|
|
key: 'failed-or-blocked',
|
|
label: 'Failed or blocked',
|
|
value: $failedOrBlocked,
|
|
description: 'Terminal execution records that need review before retrying.',
|
|
color: 'danger',
|
|
descriptionIcon: 'heroicon-m-no-symbol',
|
|
),
|
|
$this->workbenchStat(
|
|
key: 'completed-recently',
|
|
label: 'Completed recently',
|
|
value: $completedRecently,
|
|
description: 'Recent execution results, not environment or governance health.',
|
|
color: 'success',
|
|
descriptionIcon: 'heroicon-m-check-circle',
|
|
),
|
|
];
|
|
}
|
|
|
|
private function workbenchStat(
|
|
string $key,
|
|
string $label,
|
|
int $value,
|
|
string $description,
|
|
string $color,
|
|
string $descriptionIcon,
|
|
): Stat {
|
|
return Stat::make($label, (string) $value)
|
|
->description($description)
|
|
->descriptionIcon($descriptionIcon, IconPosition::Before)
|
|
->color($color)
|
|
->extraAttributes([
|
|
'data-testid' => 'operations-workbench-stat-'.$key,
|
|
'data-stat-key' => $key,
|
|
'data-stat-value' => (string) $value,
|
|
'data-stat-color' => $color,
|
|
'data-stat-label' => $label,
|
|
]);
|
|
}
|
|
|
|
private function summaryCount(callable $scope): int
|
|
{
|
|
$query = $this->scopedOperationRunQuery();
|
|
|
|
if (! $query instanceof Builder) {
|
|
return 0;
|
|
}
|
|
|
|
return (int) $scope($query)->count();
|
|
}
|
|
|
|
private function scopedOperationRunQuery(): ?Builder
|
|
{
|
|
$workspace = app(WorkspaceContext::class)->currentWorkspace(request());
|
|
$user = auth()->user();
|
|
|
|
if (! $workspace instanceof Workspace || ! $user instanceof User) {
|
|
return null;
|
|
}
|
|
|
|
$workspaceId = (int) $workspace->getKey();
|
|
$environmentFilter = WorkspaceHubEnvironmentFilter::fromRequest(request(), $workspace);
|
|
$allowedEnvironmentIds = app(ManagedEnvironmentAccessScopeResolver::class)
|
|
->allowedManagedEnvironmentIdsForWorkspace($user, $workspaceId);
|
|
|
|
return OperationRun::query()
|
|
->where('workspace_id', $workspaceId)
|
|
->when(
|
|
$allowedEnvironmentIds !== null,
|
|
function (Builder $query) use ($allowedEnvironmentIds): Builder {
|
|
return $query->where(function (Builder $query) use ($allowedEnvironmentIds): void {
|
|
$query->whereNull('managed_environment_id');
|
|
|
|
if ($allowedEnvironmentIds !== []) {
|
|
$query->orWhereIn(
|
|
'managed_environment_id',
|
|
array_values(array_unique(array_map('intval', $allowedEnvironmentIds))),
|
|
);
|
|
}
|
|
});
|
|
},
|
|
)
|
|
->when(
|
|
$environmentFilter instanceof WorkspaceHubEnvironmentFilter,
|
|
fn (Builder $query): Builder => $environmentFilter->applyToQuery($query),
|
|
);
|
|
}
|
|
}
|