202 lines
9.6 KiB
PHP
202 lines
9.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\Widgets\TenantConfiguration;
|
|
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\TenantConfigurationResource;
|
|
use App\Models\User;
|
|
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
|
|
use App\Services\TenantConfiguration\CoverageV2ReadinessReadModel;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\Badges\BadgeDomain;
|
|
use App\Support\Badges\BadgeRenderer;
|
|
use App\Support\Filament\TablePaginationProfiles;
|
|
use Filament\Actions\Action;
|
|
use Filament\Support\Enums\FontFamily;
|
|
use Filament\Tables\Columns\TextColumn;
|
|
use Filament\Tables\Filters\SelectFilter;
|
|
use Filament\Tables\Table;
|
|
use Filament\Widgets\TableWidget;
|
|
use Illuminate\Contracts\View\View;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Livewire\Attributes\Locked;
|
|
|
|
class CoverageV2ResourceInstancesTable extends TableWidget
|
|
{
|
|
protected static bool $isLazy = false;
|
|
|
|
protected int|string|array $columnSpan = 'full';
|
|
|
|
#[Locked]
|
|
public ?int $environmentId = null;
|
|
|
|
public function mount(?int $environmentId = null): void
|
|
{
|
|
$this->environmentId = $environmentId;
|
|
$this->authorizeEnvironment();
|
|
}
|
|
|
|
public function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->heading('Resource instances')
|
|
->deferLoading(false)
|
|
->query(fn (): Builder => app(CoverageV2ReadinessReadModel::class)->resourceInstanceQuery($this->environment()))
|
|
->searchable()
|
|
->searchPlaceholder('Search resources')
|
|
->paginated(TablePaginationProfiles::productSurface())
|
|
->columns([
|
|
TextColumn::make('source_display_name')
|
|
->label('Resource')
|
|
->searchable()
|
|
->sortable()
|
|
->description(fn (TenantConfigurationResource $record): string => (string) $record->canonical_resource_key)
|
|
->limit(48)
|
|
->tooltip(fn (TenantConfigurationResource $record): string => (string) ($record->source_display_name ?: $record->canonical_resource_key))
|
|
->action($this->inspectAction()),
|
|
TextColumn::make('resourceType.display_name')
|
|
->label('Resource type')
|
|
->searchable()
|
|
->sortable()
|
|
->description(fn (TenantConfigurationResource $record): string => (string) $record->canonical_type)
|
|
->wrap(),
|
|
TextColumn::make('providerConnection.display_name')
|
|
->label('Provider connection')
|
|
->sortable()
|
|
->placeholder('Unassigned provider connection')
|
|
->limit(32)
|
|
->tooltip(fn (?string $state): ?string => filled($state) ? $state : null)
|
|
->toggleable(isToggledHiddenByDefault: true),
|
|
TextColumn::make('coverage_level')
|
|
->label('Coverage level')
|
|
->state(fn (TenantConfigurationResource $record): ?string => $record->latestEvidence?->coverage_level?->value)
|
|
->badge()
|
|
->placeholder('Not captured')
|
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::CoverageV2CoverageLevel))
|
|
->color(BadgeRenderer::color(BadgeDomain::CoverageV2CoverageLevel))
|
|
->icon(BadgeRenderer::icon(BadgeDomain::CoverageV2CoverageLevel))
|
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::CoverageV2CoverageLevel)),
|
|
TextColumn::make('latest_evidence_state')
|
|
->label('Evidence state')
|
|
->badge()
|
|
->sortable()
|
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::CoverageV2EvidenceState))
|
|
->color(BadgeRenderer::color(BadgeDomain::CoverageV2EvidenceState))
|
|
->icon(BadgeRenderer::icon(BadgeDomain::CoverageV2EvidenceState))
|
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::CoverageV2EvidenceState)),
|
|
TextColumn::make('latest_identity_state')
|
|
->label('Identity state')
|
|
->badge()
|
|
->sortable()
|
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::CoverageV2IdentityState))
|
|
->color(BadgeRenderer::color(BadgeDomain::CoverageV2IdentityState))
|
|
->icon(BadgeRenderer::icon(BadgeDomain::CoverageV2IdentityState))
|
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::CoverageV2IdentityState)),
|
|
TextColumn::make('latest_claim_state')
|
|
->label('Claim state')
|
|
->badge()
|
|
->sortable()
|
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::CoverageV2ClaimState))
|
|
->color(BadgeRenderer::color(BadgeDomain::CoverageV2ClaimState))
|
|
->icon(BadgeRenderer::icon(BadgeDomain::CoverageV2ClaimState))
|
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::CoverageV2ClaimState)),
|
|
TextColumn::make('latest_captured_at')
|
|
->label('Last captured')
|
|
->dateTime()
|
|
->sortable()
|
|
->placeholder('Not captured'),
|
|
TextColumn::make('latest_payload_hash')
|
|
->label('Evidence hash')
|
|
->state(fn (TenantConfigurationResource $record): ?string => $record->latestEvidence?->payload_hash ?: $record->latest_payload_hash)
|
|
->copyable()
|
|
->limit(16)
|
|
->fontFamily(FontFamily::Mono)
|
|
->placeholder('No hash')
|
|
->toggleable(isToggledHiddenByDefault: true),
|
|
])
|
|
->filters([
|
|
SelectFilter::make('resource_type_id')
|
|
->label('Resource type')
|
|
->options(fn (): array => \App\Models\TenantConfigurationResourceType::query()
|
|
->active()
|
|
->orderBy('display_name')
|
|
->pluck('display_name', 'id')
|
|
->mapWithKeys(fn (string $label, int|string $id): array => [(string) $id => $label])
|
|
->all()),
|
|
SelectFilter::make('provider_connection_id')
|
|
->label('Provider connection')
|
|
->options(fn (): array => app(CoverageV2ReadinessReadModel::class)->providerConnectionOptions($this->environment())),
|
|
SelectFilter::make('coverage_level')
|
|
->label('Coverage level')
|
|
->options(CoverageV2ReadinessReadModel::coverageLevelOptions())
|
|
->query(fn (Builder $query, array $data): Builder => filled($data['value'] ?? null)
|
|
? $query->whereHas('latestEvidence', fn (Builder $latestEvidence): Builder => $latestEvidence->where('coverage_level', $data['value']))
|
|
: $query),
|
|
SelectFilter::make('latest_evidence_state')
|
|
->label('Evidence state')
|
|
->options(CoverageV2ReadinessReadModel::evidenceStateOptions()),
|
|
SelectFilter::make('latest_identity_state')
|
|
->label('Identity state')
|
|
->options(CoverageV2ReadinessReadModel::identityStateOptions()),
|
|
SelectFilter::make('latest_claim_state')
|
|
->label('Claim state')
|
|
->options(CoverageV2ReadinessReadModel::claimStateOptions()),
|
|
SelectFilter::make('source_class')
|
|
->label('Source class')
|
|
->options(CoverageV2ReadinessReadModel::sourceClassOptions()),
|
|
])
|
|
->recordUrl(null)
|
|
->bulkActions([])
|
|
->emptyStateHeading('No captured Coverage v2 resources')
|
|
->emptyStateDescription('No Coverage v2 resource rows match this environment and provider scope. Capture prerequisites must be satisfied before this page can support activation-readiness review.')
|
|
->emptyStateIcon('heroicon-o-shield-exclamation');
|
|
}
|
|
|
|
private function environment(): ManagedEnvironment
|
|
{
|
|
$environment = ManagedEnvironment::query()
|
|
->whereKey($this->environmentId)
|
|
->first();
|
|
|
|
if (! $environment instanceof ManagedEnvironment) {
|
|
abort(404);
|
|
}
|
|
|
|
return $environment;
|
|
}
|
|
|
|
private function authorizeEnvironment(): void
|
|
{
|
|
$environment = $this->environment();
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
abort(403);
|
|
}
|
|
|
|
$decision = app(ManagedEnvironmentAccessScopeResolver::class)
|
|
->decision($user, $environment, Capabilities::EVIDENCE_VIEW);
|
|
|
|
if (! $decision->allowed()) {
|
|
abort($decision->denialHttpStatus ?? 403);
|
|
}
|
|
}
|
|
|
|
private function inspectAction(): Action
|
|
{
|
|
return Action::make('inspect')
|
|
->label('Inspect')
|
|
->icon('heroicon-o-eye')
|
|
->color('gray')
|
|
->slideOver()
|
|
->modalSubmitAction(false)
|
|
->modalCancelActionLabel('Close')
|
|
->modalHeading(fn (TenantConfigurationResource $record): string => (string) ($record->source_display_name ?: $record->canonical_resource_key))
|
|
->modalContent(fn (TenantConfigurationResource $record): View => view('filament.modals.tenant-configuration.coverage-v2-resource-inspect', [
|
|
'details' => app(CoverageV2ReadinessReadModel::class)->inspectDetails($record, $this->environment(), auth()->user()),
|
|
]));
|
|
}
|
|
}
|