224 lines
8.9 KiB
PHP
224 lines
8.9 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Resources\PolicyResource\RelationManagers;
|
|
|
|
use App\Filament\Concerns\ResolvesPanelTenantContext;
|
|
use App\Filament\Resources\PolicyVersionResource;
|
|
use App\Filament\Resources\RestoreRunResource;
|
|
use App\Models\Policy;
|
|
use App\Models\PolicyVersion;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Services\Auth\CapabilityResolver;
|
|
use App\Services\Intune\RestoreService;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\Badges\TagBadgeDomain;
|
|
use App\Support\Badges\TagBadgeRenderer;
|
|
use App\Support\Rbac\UiEnforcement;
|
|
use App\Support\Rbac\UiTooltips;
|
|
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
|
|
use Filament\Actions;
|
|
use Filament\Forms;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Resources\RelationManagers\RelationManager;
|
|
use Filament\Tables;
|
|
use Filament\Tables\Table;
|
|
|
|
class VersionsRelationManager extends RelationManager
|
|
{
|
|
use ResolvesPanelTenantContext;
|
|
|
|
protected static string $relationship = 'versions';
|
|
|
|
/**
|
|
* @param array<string, mixed> $arguments
|
|
* @param array<string, mixed> $context
|
|
*/
|
|
public function mountAction(string $name, array $arguments = [], array $context = []): mixed
|
|
{
|
|
if (($context['table'] ?? false) === true && $name === 'restore_to_intune' && filled($context['recordKey'] ?? null)) {
|
|
$this->resolveOwnerScopedVersionRecord($this->getOwnerRecord(), $context['recordKey']);
|
|
}
|
|
|
|
return parent::mountAction($name, $arguments, $context);
|
|
}
|
|
|
|
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
|
{
|
|
return ActionSurfaceDeclaration::forRelationManager(ActionSurfaceProfile::RelationManager)
|
|
->exempt(ActionSurfaceSlot::ListHeader, 'Versions sub-list intentionally has no header actions.')
|
|
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value)
|
|
->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'Clickable-row inspection stays primary while restore remains the only inline row shortcut.')
|
|
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'Bulk actions are intentionally omitted for version restore safety.')
|
|
->exempt(ActionSurfaceSlot::ListEmptyState, 'No inline empty-state action is exposed in this embedded relation manager.');
|
|
}
|
|
|
|
public function table(Table $table): Table
|
|
{
|
|
$restoreToIntune = Actions\Action::make('restore_to_intune')
|
|
->label($this->text('relation.restore_to_microsoft_intune'))
|
|
->icon('heroicon-o-arrow-path-rounded-square')
|
|
->color('danger')
|
|
->requiresConfirmation()
|
|
->modalHeading(fn (PolicyVersion $record): string => $this->text('relation.restore_heading', ['version' => $record->version_number]))
|
|
->modalSubheading($this->text('relation.restore_subheading'))
|
|
->form([
|
|
Forms\Components\Toggle::make('is_dry_run')
|
|
->label($this->text('common.preview_only_dry_run'))
|
|
->default(true),
|
|
])
|
|
->action(function (mixed $record, array $data, RestoreService $restoreService) {
|
|
$record = $this->resolveOwnerScopedVersionRecord($this->getOwnerRecord(), $record);
|
|
$tenant = static::resolveTenantContextForCurrentPanel();
|
|
$user = auth()->user();
|
|
|
|
if (! $tenant instanceof Tenant || ! $user instanceof User) {
|
|
Notification::make()
|
|
->title($this->text('relation.missing_context_title'))
|
|
->danger()
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
if ($record->tenant_id !== $tenant->id) {
|
|
Notification::make()
|
|
->title($this->text('versions.different_tenant_title'))
|
|
->danger()
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$run = $restoreService->executeFromPolicyVersion(
|
|
tenant: $tenant,
|
|
version: $record,
|
|
dryRun: (bool) ($data['is_dry_run'] ?? true),
|
|
actorEmail: $user->email,
|
|
actorName: $user->name,
|
|
);
|
|
} catch (\Throwable $throwable) {
|
|
Notification::make()
|
|
->title($this->text('relation.restore_run_failed_title'))
|
|
->body($throwable->getMessage())
|
|
->danger()
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
Notification::make()
|
|
->title($this->text('relation.restore_run_started_title'))
|
|
->success()
|
|
->send();
|
|
|
|
return redirect(RestoreRunResource::getUrl('view', ['record' => $run]));
|
|
});
|
|
|
|
UiEnforcement::forAction($restoreToIntune)
|
|
->requireCapability(Capabilities::TENANT_MANAGE)
|
|
->apply();
|
|
|
|
$restoreToIntune
|
|
->disabled(function (PolicyVersion $record): bool {
|
|
if (($record->metadata['source'] ?? null) === 'metadata_only') {
|
|
return true;
|
|
}
|
|
|
|
$tenant = static::resolveTenantContextForCurrentPanel();
|
|
$user = auth()->user();
|
|
|
|
if (! $tenant instanceof Tenant || ! $user instanceof User) {
|
|
return true;
|
|
}
|
|
|
|
$resolver = app(CapabilityResolver::class);
|
|
|
|
if (! $resolver->isMember($user, $tenant)) {
|
|
return true;
|
|
}
|
|
|
|
return ! $resolver->can($user, $tenant, Capabilities::TENANT_MANAGE);
|
|
})
|
|
->tooltip(function (PolicyVersion $record): ?string {
|
|
if (($record->metadata['source'] ?? null) === 'metadata_only') {
|
|
return $this->text('versions.metadata_only_tooltip');
|
|
}
|
|
|
|
$tenant = static::resolveTenantContextForCurrentPanel();
|
|
$user = auth()->user();
|
|
|
|
if (! $tenant instanceof Tenant || ! $user instanceof User) {
|
|
return null;
|
|
}
|
|
|
|
$resolver = app(CapabilityResolver::class);
|
|
|
|
if (! $resolver->isMember($user, $tenant)) {
|
|
return null;
|
|
}
|
|
|
|
if (! $resolver->can($user, $tenant, Capabilities::TENANT_MANAGE)) {
|
|
return UiTooltips::INSUFFICIENT_PERMISSION;
|
|
}
|
|
|
|
return null;
|
|
});
|
|
|
|
return $table
|
|
->columns([
|
|
Tables\Columns\TextColumn::make('version_number')->label($this->text('common.version'))->sortable(),
|
|
Tables\Columns\TextColumn::make('captured_at')->label($this->text('common.captured'))->dateTime()->sortable(),
|
|
Tables\Columns\TextColumn::make('created_by')->label($this->text('common.actor')),
|
|
Tables\Columns\TextColumn::make('policy_type')
|
|
->label($this->text('common.type'))
|
|
->badge()
|
|
->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::PolicyType))
|
|
->color(TagBadgeRenderer::color(TagBadgeDomain::PolicyType))
|
|
->toggleable(isToggledHiddenByDefault: true),
|
|
])
|
|
->defaultSort('version_number', 'desc')
|
|
->paginated(\App\Support\Filament\TablePaginationProfiles::relationManager())
|
|
->recordUrl(fn (PolicyVersion $record): string => PolicyVersionResource::getUrl('view', ['record' => $record]))
|
|
->filters([])
|
|
->headerActions([])
|
|
->actions([
|
|
$restoreToIntune,
|
|
])
|
|
->bulkActions([])
|
|
->emptyStateHeading($this->text('relation.no_versions_captured'))
|
|
->emptyStateDescription($this->text('relation.no_versions_captured_description'));
|
|
}
|
|
|
|
private function resolveOwnerScopedVersionRecord(Policy $policy, mixed $record): PolicyVersion
|
|
{
|
|
$recordId = $record instanceof PolicyVersion
|
|
? (int) $record->getKey()
|
|
: (is_numeric($record) ? (int) $record : 0);
|
|
|
|
if ($recordId <= 0) {
|
|
abort(404);
|
|
}
|
|
|
|
$resolvedRecord = $policy->versions()
|
|
->where('tenant_id', (int) $policy->tenant_id)
|
|
->whereKey($recordId)
|
|
->first();
|
|
|
|
if (! $resolvedRecord instanceof PolicyVersion) {
|
|
abort(404);
|
|
}
|
|
|
|
return $resolvedRecord;
|
|
}
|
|
|
|
private function text(string $key, array $replace = []): string
|
|
{
|
|
return __('localization.policy.'.$key, $replace);
|
|
}
|
|
}
|