## Summary - introduce a shared operator outcome taxonomy with semantic axes, severity bands, and next-action policy - apply the taxonomy to operations, evidence/review completeness, baseline semantics, and restore semantics - harden badge rendering, tenant-safe filtering/search behavior, and operator-facing summary/notification wording - add the spec kit artifacts, reference documentation, and regression coverage for diagnostic-vs-primary state handling ## Testing - focused Pest coverage for taxonomy registry and badge guardrails - operations presentation and notification tests - evidence, baseline, restore, and tenant-scope regression tests ## Notes - Livewire v4.0+ compliance is preserved in the existing Filament v5 stack - panel provider registration remains unchanged in bootstrap/providers.php - no new globally searchable resource was added; adopted resources remain tenant-safe and out of global search where required - no new destructive action family was introduced; existing actions keep their current authorization and confirmation behavior - no new frontend asset strategy was introduced; existing deploy flow with filament:assets remains unchanged Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #186
191 lines
6.5 KiB
PHP
191 lines
6.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\Pages\Monitoring;
|
|
|
|
use App\Filament\Resources\OperationRunResource;
|
|
use App\Filament\Widgets\Operations\OperationsKpiHeader;
|
|
use App\Models\OperationRun;
|
|
use App\Models\Tenant;
|
|
use App\Support\Filament\CanonicalAdminTenantFilterState;
|
|
use App\Support\Navigation\CanonicalNavigationContext;
|
|
use App\Support\OperateHub\OperateHubShell;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
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 UnitEnum;
|
|
|
|
class Operations extends Page implements HasForms, HasTable
|
|
{
|
|
use InteractsWithForms;
|
|
use InteractsWithTable;
|
|
|
|
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 function mount(): void
|
|
{
|
|
$this->navigationContextPayload = is_array(request()->query('nav')) ? request()->query('nav') : null;
|
|
|
|
app(CanonicalAdminTenantFilterState::class)->sync(
|
|
$this->getTableFiltersSessionKey(),
|
|
['type', 'initiator_name'],
|
|
request(),
|
|
);
|
|
|
|
$this->mountInteractsWithTable();
|
|
}
|
|
|
|
protected function getHeaderWidgets(): array
|
|
{
|
|
return [
|
|
OperationsKpiHeader::class,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<Action>
|
|
*/
|
|
protected function getHeaderActions(): array
|
|
{
|
|
$operateHubShell = app(OperateHubShell::class);
|
|
$navigationContext = $this->navigationContext();
|
|
|
|
$actions = [
|
|
Action::make('operate_hub_scope_operations')
|
|
->label($operateHubShell->scopeLabel(request()))
|
|
->color('gray')
|
|
->disabled(),
|
|
];
|
|
|
|
$activeTenant = $operateHubShell->activeEntitledTenant(request());
|
|
|
|
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 ($activeTenant instanceof Tenant) {
|
|
$actions[] = Action::make('operate_hub_back_to_tenant_operations')
|
|
->label('Back to '.$activeTenant->name)
|
|
->icon('heroicon-o-arrow-left')
|
|
->color('gray')
|
|
->url(\App\Filament\Pages\TenantDashboard::getUrl(panel: 'tenant', tenant: $activeTenant));
|
|
}
|
|
|
|
if ($activeTenant instanceof Tenant) {
|
|
$actions[] = Action::make('operate_hub_show_all_tenants')
|
|
->label('Show all tenants')
|
|
->color('gray')
|
|
->action(function (): void {
|
|
Filament::setTenant(null, true);
|
|
|
|
app(WorkspaceContext::class)->clearLastTenantId(request());
|
|
|
|
$this->removeTableFilter('tenant_id');
|
|
|
|
$this->redirect('/admin/operations');
|
|
});
|
|
}
|
|
|
|
return $actions;
|
|
}
|
|
|
|
private function navigationContext(): ?CanonicalNavigationContext
|
|
{
|
|
if (! is_array($this->navigationContextPayload)) {
|
|
return CanonicalNavigationContext::fromRequest(request());
|
|
}
|
|
|
|
$request = request()->duplicate(query: ['nav' => $this->navigationContextPayload]);
|
|
|
|
return CanonicalNavigationContext::fromRequest($request);
|
|
}
|
|
|
|
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 = data_get($this->tableFilters, 'tenant_id.value');
|
|
|
|
if (! is_numeric($tenantFilter)) {
|
|
$tenantFilter = data_get(session()->get($this->getTableFiltersSessionKey(), []), 'tenant_id.value');
|
|
}
|
|
|
|
$query = OperationRun::query()
|
|
->with('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(
|
|
is_numeric($tenantFilter),
|
|
fn (Builder $query): Builder => $query->where('tenant_id', (int) $tenantFilter),
|
|
);
|
|
|
|
return $this->applyActiveTab($query);
|
|
});
|
|
}
|
|
|
|
private function applyActiveTab(Builder $query): Builder
|
|
{
|
|
return match ($this->activeTab) {
|
|
'active' => $query->whereIn('status', [
|
|
OperationRunStatus::Queued->value,
|
|
OperationRunStatus::Running->value,
|
|
]),
|
|
'blocked' => $query
|
|
->where('status', OperationRunStatus::Completed->value)
|
|
->where('outcome', OperationRunOutcome::Blocked->value),
|
|
'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,
|
|
};
|
|
}
|
|
}
|