TenantAtlas/app/Livewire/BackupSetPolicyPickerTable.php
ahmido 412dd7ad66 feat/017-policy-types-mam-endpoint-security-baselines (#23)
Hydrate configurationPolicies/{id}/settings for endpoint security/baseline policies so snapshots include real rule data.
Treat those types like Settings Catalog policies in the normalizer so they show the searchable settings table, recognizable categories, and readable choice values (firewall-specific formatting + interface badge parsing).
Improve “General” tab cards: badge lists for platforms/technologies, template reference summary (name/family/version/ID), and ISO timestamps rendered as YYYY‑MM‑DD HH:MM:SS; added regression test for the view.

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local>
Reviewed-on: #23
2026-01-03 02:06:35 +00:00

278 lines
10 KiB
PHP

<?php
namespace App\Livewire;
use App\Models\BackupSet;
use App\Models\Policy;
use App\Models\Tenant;
use App\Services\Intune\BackupService;
use Filament\Actions\BulkAction;
use Filament\Notifications\Notification;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Table;
use Filament\Tables\TableComponent;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class BackupSetPolicyPickerTable extends TableComponent
{
public int $backupSetId;
public bool $include_assignments = true;
public bool $include_scope_tags = true;
public bool $include_foundations = true;
public function mount(int $backupSetId): void
{
$this->backupSetId = $backupSetId;
}
public static function externalIdShort(?string $externalId): string
{
$value = (string) ($externalId ?? '');
$normalized = preg_replace('/[^A-Za-z0-9]/', '', $value) ?? '';
if ($normalized === '') {
return '—';
}
return substr($normalized, -8);
}
public function table(Table $table): Table
{
$backupSet = BackupSet::query()->find($this->backupSetId);
$tenantId = $backupSet?->tenant_id ?? Tenant::current()->getKey();
$existingPolicyIds = $backupSet
? $backupSet->items()->pluck('policy_id')->filter()->all()
: [];
return $table
->queryStringIdentifier('backupSetPolicyPicker'.Str::studly((string) $this->backupSetId))
->query(
Policy::query()
->where('tenant_id', $tenantId)
->when($existingPolicyIds !== [], fn (Builder $query) => $query->whereNotIn('id', $existingPolicyIds))
)
->deferLoading(! app()->runningUnitTests())
->paginated([25, 50, 100])
->defaultPaginationPageOption(25)
->searchable()
->striped()
->columns([
TextColumn::make('display_name')
->label('Name')
->searchable()
->sortable()
->wrap(),
TextColumn::make('policy_type')
->label('Type')
->badge()
->formatStateUsing(fn (?string $state): string => (string) (static::typeMeta($state)['label'] ?? $state ?? '—')),
TextColumn::make('platform')
->label('Platform')
->badge()
->default('—')
->sortable(),
TextColumn::make('external_id')
->label('External ID')
->formatStateUsing(fn (?string $state): string => static::externalIdShort($state))
->tooltip(fn (?string $state): ?string => filled($state) ? $state : null)
->extraAttributes(['class' => 'font-mono text-xs'])
->toggleable(),
TextColumn::make('versions_count')
->label('Versions')
->state(fn (Policy $record): int => (int) ($record->versions_count ?? 0))
->badge()
->sortable(),
TextColumn::make('last_synced_at')
->label('Last synced')
->dateTime()
->since()
->sortable()
->toggleable(),
TextColumn::make('ignored_at')
->label('Ignored')
->badge()
->color(fn (?string $state): string => filled($state) ? 'warning' : 'gray')
->formatStateUsing(fn (?string $state): string => filled($state) ? 'yes' : 'no')
->toggleable(isToggledHiddenByDefault: true),
])
->modifyQueryUsing(fn (Builder $query) => $query->withCount('versions'))
->filters([
SelectFilter::make('policy_type')
->label('Policy type')
->options(static::policyTypeOptions()),
SelectFilter::make('platform')
->label('Platform')
->options(fn (): array => Policy::query()
->where('tenant_id', $tenantId)
->whereNotNull('platform')
->distinct()
->orderBy('platform')
->pluck('platform', 'platform')
->all()),
SelectFilter::make('synced_within')
->label('Last synced')
->options([
'7' => 'Within 7 days',
'30' => 'Within 30 days',
'90' => 'Within 90 days',
'any' => 'Any time',
])
->default('7')
->query(function (Builder $query, array $data): Builder {
$value = (string) ($data['value'] ?? '7');
if ($value === 'any') {
return $query;
}
$days = is_numeric($value) ? (int) $value : 7;
return $query->where('last_synced_at', '>', now()->subDays(max(1, $days)));
}),
TernaryFilter::make('ignored')
->label('Ignored')
->nullable()
->queries(
true: fn (Builder $query) => $query->whereNotNull('ignored_at'),
false: fn (Builder $query) => $query->whereNull('ignored_at'),
)
->default(false),
SelectFilter::make('has_versions')
->label('Has versions')
->options([
'1' => 'Has versions',
'0' => 'No versions',
])
->query(function (Builder $query, array $data): Builder {
$value = $data['value'] ?? null;
if ($value === null || $value === '') {
return $query;
}
return match ((string) $value) {
'1' => $query->whereHas('versions'),
'0' => $query->whereDoesntHave('versions'),
default => $query,
};
}),
])
->bulkActions([
BulkAction::make('add_selected_to_backup_set')
->label('Add selected')
->icon('heroicon-m-plus')
->action(function (Collection $records, BackupService $service): void {
$backupSet = BackupSet::query()->findOrFail($this->backupSetId);
$tenant = $backupSet->tenant ?? Tenant::current();
$beforeFailures = (array) (($backupSet->metadata ?? [])['failures'] ?? []);
$beforeFailureCount = count($beforeFailures);
$policyIds = $records->pluck('id')->all();
if ($policyIds === []) {
Notification::make()
->title('No policies selected')
->warning()
->send();
return;
}
$service->addPoliciesToSet(
tenant: $tenant,
backupSet: $backupSet,
policyIds: $policyIds,
actorEmail: auth()->user()?->email,
actorName: auth()->user()?->name,
includeAssignments: $this->include_assignments,
includeScopeTags: $this->include_scope_tags,
includeFoundations: $this->include_foundations,
);
$notificationTitle = $this->include_foundations
? 'Backup items added'
: 'Policies added to backup';
$backupSet->refresh();
$afterFailures = (array) (($backupSet->metadata ?? [])['failures'] ?? []);
$afterFailureCount = count($afterFailures);
if ($afterFailureCount > $beforeFailureCount) {
Notification::make()
->title($notificationTitle.' with failures')
->body('Some policies could not be captured from Microsoft Graph. Check the backup set failures list for details.')
->warning()
->send();
} else {
Notification::make()
->title($notificationTitle)
->success()
->send();
}
$this->resetTable();
}),
]);
}
public function render(): View
{
return view('livewire.backup-set-policy-picker-table');
}
/**
* @return array{label:?string,category:?string,restore:?string,risk:?string}|array<string,mixed>
*/
private static function typeMeta(?string $type): array
{
if ($type === null) {
return [];
}
$types = array_merge(
config('tenantpilot.supported_policy_types', []),
config('tenantpilot.foundation_types', [])
);
return collect($types)
->firstWhere('type', $type) ?? [];
}
/**
* @return array<string, string>
*/
private static function policyTypeOptions(): array
{
$types = array_merge(
config('tenantpilot.supported_policy_types', []),
config('tenantpilot.foundation_types', [])
);
return collect($types)
->mapWithKeys(function (array $meta): array {
$type = (string) ($meta['type'] ?? '');
if ($type === '') {
return [];
}
$label = (string) ($meta['label'] ?? $type);
return [$type => $label];
})
->all();
}
}