merge: agent session work
This commit is contained in:
commit
9d530d8add
@ -9,6 +9,7 @@
|
|||||||
use App\Jobs\ExecuteRestoreRunJob;
|
use App\Jobs\ExecuteRestoreRunJob;
|
||||||
use App\Models\BackupItem;
|
use App\Models\BackupItem;
|
||||||
use App\Models\BackupSet;
|
use App\Models\BackupSet;
|
||||||
|
use App\Models\EntraGroup;
|
||||||
use App\Models\RestoreRun;
|
use App\Models\RestoreRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Rules\SkipOrUuidRule;
|
use App\Rules\SkipOrUuidRule;
|
||||||
@ -112,7 +113,21 @@ public static function form(Schema $schema): Schema
|
|||||||
tenant: $tenant
|
tenant: $tenant
|
||||||
);
|
);
|
||||||
|
|
||||||
return array_map(function (array $group): Forms\Components\TextInput {
|
$groupCacheQuery = EntraGroup::query()->where('tenant_id', $tenant->getKey());
|
||||||
|
$hasCachedGroups = $groupCacheQuery->exists();
|
||||||
|
|
||||||
|
$stalenessDays = (int) config('directory_groups.staleness_days', 30);
|
||||||
|
$cutoff = now('UTC')->subDays(max(1, $stalenessDays));
|
||||||
|
$latestSeen = $groupCacheQuery->max('last_seen_at');
|
||||||
|
$isStale = $hasCachedGroups && (! $latestSeen || $latestSeen < $cutoff);
|
||||||
|
|
||||||
|
$cacheNotice = match (true) {
|
||||||
|
! $hasCachedGroups => 'No cached groups found. Run "Sync Groups" first.',
|
||||||
|
$isStale => "Cached groups may be stale (>${stalenessDays} days). Consider running \"Sync Groups\".",
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return array_map(function (array $group) use ($cacheNotice): Forms\Components\TextInput {
|
||||||
$groupId = $group['id'];
|
$groupId = $group['id'];
|
||||||
$label = $group['label'];
|
$label = $group['label'];
|
||||||
|
|
||||||
@ -121,7 +136,28 @@ public static function form(Schema $schema): Schema
|
|||||||
->placeholder('SKIP or target group Object ID (GUID)')
|
->placeholder('SKIP or target group Object ID (GUID)')
|
||||||
->rules([new SkipOrUuidRule])
|
->rules([new SkipOrUuidRule])
|
||||||
->required()
|
->required()
|
||||||
->helperText('Paste the target Entra ID group Object ID (GUID). Labels use the cached directory groups only (no live Graph lookups). Use SKIP to omit the assignment.');
|
->suffixAction(
|
||||||
|
Actions\Action::make('select_from_directory_cache_'.str_replace('-', '_', $groupId))
|
||||||
|
->icon('heroicon-o-magnifying-glass')
|
||||||
|
->iconButton()
|
||||||
|
->tooltip('Select from Directory cache')
|
||||||
|
->modalHeading('Select from Directory cache')
|
||||||
|
->modalWidth('5xl')
|
||||||
|
->modalSubmitAction(false)
|
||||||
|
->modalCancelActionLabel('Close')
|
||||||
|
->modalContent(fn () => view('filament.modals.entra-group-cache-picker', [
|
||||||
|
'sourceGroupId' => $groupId,
|
||||||
|
]))
|
||||||
|
)
|
||||||
|
->helperText(fn (): string => $cacheNotice ? ($cacheNotice.' Labels use cached directory groups only (no live Graph lookups). Paste a GUID or use SKIP.') : 'Paste the target Entra ID group Object ID (GUID). Labels use cached directory groups only (no live Graph lookups). Use SKIP to omit the assignment.')
|
||||||
|
->hintAction(
|
||||||
|
Actions\Action::make('open_directory_groups_'.str_replace('-', '_', $groupId))
|
||||||
|
->label('Sync Groups')
|
||||||
|
->icon('heroicon-o-arrow-path')
|
||||||
|
->color('warning')
|
||||||
|
->url(fn (): string => EntraGroupResource::getUrl('index', tenant: Tenant::current()))
|
||||||
|
->visible(fn (): bool => $cacheNotice !== null)
|
||||||
|
);
|
||||||
}, $unresolved);
|
}, $unresolved);
|
||||||
})
|
})
|
||||||
->visible(function (Get $get): bool {
|
->visible(function (Get $get): bool {
|
||||||
@ -318,7 +354,21 @@ public static function getWizardSteps(): array
|
|||||||
tenant: $tenant
|
tenant: $tenant
|
||||||
);
|
);
|
||||||
|
|
||||||
return array_map(function (array $group): Forms\Components\TextInput {
|
$groupCacheQuery = EntraGroup::query()->where('tenant_id', $tenant->getKey());
|
||||||
|
$hasCachedGroups = $groupCacheQuery->exists();
|
||||||
|
|
||||||
|
$stalenessDays = (int) config('directory_groups.staleness_days', 30);
|
||||||
|
$cutoff = now('UTC')->subDays(max(1, $stalenessDays));
|
||||||
|
$latestSeen = $groupCacheQuery->max('last_seen_at');
|
||||||
|
$isStale = $hasCachedGroups && (! $latestSeen || $latestSeen < $cutoff);
|
||||||
|
|
||||||
|
$cacheNotice = match (true) {
|
||||||
|
! $hasCachedGroups => 'No cached groups found. Run "Sync Groups" first.',
|
||||||
|
$isStale => "Cached groups may be stale (>${stalenessDays} days). Consider running \"Sync Groups\".",
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return array_map(function (array $group) use ($cacheNotice): Forms\Components\TextInput {
|
||||||
$groupId = $group['id'];
|
$groupId = $group['id'];
|
||||||
$label = $group['label'];
|
$label = $group['label'];
|
||||||
|
|
||||||
@ -336,7 +386,28 @@ public static function getWizardSteps(): array
|
|||||||
$set('preview_ran_at', null);
|
$set('preview_ran_at', null);
|
||||||
})
|
})
|
||||||
->required()
|
->required()
|
||||||
->helperText('Paste the target Entra ID group Object ID (GUID). Names are not resolved in this phase. Use SKIP to omit the assignment.');
|
->suffixAction(
|
||||||
|
Actions\Action::make('select_from_directory_cache_'.str_replace('-', '_', $groupId))
|
||||||
|
->icon('heroicon-o-magnifying-glass')
|
||||||
|
->iconButton()
|
||||||
|
->tooltip('Select from Directory cache')
|
||||||
|
->modalHeading('Select from Directory cache')
|
||||||
|
->modalWidth('5xl')
|
||||||
|
->modalSubmitAction(false)
|
||||||
|
->modalCancelActionLabel('Close')
|
||||||
|
->modalContent(fn () => view('filament.modals.entra-group-cache-picker', [
|
||||||
|
'sourceGroupId' => $groupId,
|
||||||
|
]))
|
||||||
|
)
|
||||||
|
->helperText(fn (): string => $cacheNotice ? ($cacheNotice.' Labels use cached directory groups only (no live Graph lookups). Paste a GUID or use SKIP.') : 'Paste the target Entra ID group Object ID (GUID). Labels use cached directory groups only (no live Graph lookups). Use SKIP to omit the assignment.')
|
||||||
|
->hintAction(
|
||||||
|
Actions\Action::make('open_directory_groups_'.str_replace('-', '_', $groupId))
|
||||||
|
->label('Sync Groups')
|
||||||
|
->icon('heroicon-o-arrow-path')
|
||||||
|
->color('warning')
|
||||||
|
->url(fn (): string => EntraGroupResource::getUrl('index', tenant: Tenant::current()))
|
||||||
|
->visible(fn (): bool => $cacheNotice !== null)
|
||||||
|
);
|
||||||
}, $unresolved);
|
}, $unresolved);
|
||||||
})
|
})
|
||||||
->visible(function (Get $get): bool {
|
->visible(function (Get $get): bool {
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
use Filament\Resources\Pages\Concerns\HasWizard;
|
use Filament\Resources\Pages\Concerns\HasWizard;
|
||||||
use Filament\Resources\Pages\CreateRecord;
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
|
|
||||||
class CreateRestoreRun extends CreateRecord
|
class CreateRestoreRun extends CreateRecord
|
||||||
{
|
{
|
||||||
@ -119,4 +120,23 @@ protected function handleRecordCreation(array $data): Model
|
|||||||
{
|
{
|
||||||
return RestoreRunResource::createRestoreRun($data);
|
return RestoreRunResource::createRestoreRun($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[On('entra-group-cache-picked')]
|
||||||
|
public function applyEntraGroupCachePick(string $sourceGroupId, string $entraId): void
|
||||||
|
{
|
||||||
|
data_set($this->data, "group_mapping.{$sourceGroupId}", $entraId);
|
||||||
|
|
||||||
|
$this->data['check_summary'] = null;
|
||||||
|
$this->data['check_results'] = [];
|
||||||
|
$this->data['checks_ran_at'] = null;
|
||||||
|
$this->data['preview_summary'] = null;
|
||||||
|
$this->data['preview_diffs'] = [];
|
||||||
|
$this->data['preview_ran_at'] = null;
|
||||||
|
|
||||||
|
$this->form->fill($this->data);
|
||||||
|
|
||||||
|
if (method_exists($this, 'unmountAction')) {
|
||||||
|
$this->unmountAction();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
203
app/Livewire/EntraGroupCachePickerTable.php
Normal file
203
app/Livewire/EntraGroupCachePickerTable.php
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Filament\Resources\EntraGroupResource;
|
||||||
|
use App\Filament\Resources\EntraGroupSyncRunResource;
|
||||||
|
use App\Models\EntraGroup;
|
||||||
|
use App\Models\Tenant;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Filament\Tables\TableComponent;
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
|
class EntraGroupCachePickerTable extends TableComponent
|
||||||
|
{
|
||||||
|
public string $sourceGroupId;
|
||||||
|
|
||||||
|
public function mount(string $sourceGroupId): void
|
||||||
|
{
|
||||||
|
$this->sourceGroupId = $sourceGroupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
$tenantId = Tenant::current()?->getKey();
|
||||||
|
|
||||||
|
$query = EntraGroup::query();
|
||||||
|
|
||||||
|
if ($tenantId) {
|
||||||
|
$query->where('tenant_id', $tenantId);
|
||||||
|
} else {
|
||||||
|
$query->whereRaw('1 = 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stalenessDays = (int) config('directory_groups.staleness_days', 30);
|
||||||
|
$cutoff = now('UTC')->subDays(max(1, $stalenessDays));
|
||||||
|
|
||||||
|
return $table
|
||||||
|
->queryStringIdentifier('entraGroupCachePicker')
|
||||||
|
->query($query)
|
||||||
|
->defaultSort('display_name')
|
||||||
|
->paginated([10, 25, 50])
|
||||||
|
->defaultPaginationPageOption(10)
|
||||||
|
->searchable()
|
||||||
|
->searchPlaceholder('Search groups…')
|
||||||
|
->deferLoading(! app()->runningUnitTests())
|
||||||
|
->columns([
|
||||||
|
TextColumn::make('display_name')
|
||||||
|
->label('Name')
|
||||||
|
->searchable()
|
||||||
|
->sortable()
|
||||||
|
->wrap()
|
||||||
|
->limit(60),
|
||||||
|
TextColumn::make('type')
|
||||||
|
->label('Type')
|
||||||
|
->badge()
|
||||||
|
->state(fn (EntraGroup $record): string => $this->groupTypeLabel($this->groupType($record)))
|
||||||
|
->color(fn (EntraGroup $record): string => $this->groupTypeColor($this->groupType($record)))
|
||||||
|
->toggleable(),
|
||||||
|
TextColumn::make('entra_id')
|
||||||
|
->label('ID')
|
||||||
|
->formatStateUsing(fn (?string $state): string => filled($state) ? ('…'.substr($state, -8)) : '—')
|
||||||
|
->extraAttributes(['class' => 'font-mono'])
|
||||||
|
->toggleable(),
|
||||||
|
TextColumn::make('last_seen_at')
|
||||||
|
->label('Last seen')
|
||||||
|
->since()
|
||||||
|
->sortable(),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
SelectFilter::make('stale')
|
||||||
|
->label('Stale')
|
||||||
|
->options([
|
||||||
|
'1' => 'Stale',
|
||||||
|
'0' => 'Fresh',
|
||||||
|
])
|
||||||
|
->query(function (Builder $query, array $data) use ($cutoff): Builder {
|
||||||
|
$value = $data['value'] ?? null;
|
||||||
|
|
||||||
|
if ($value === null || $value === '') {
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((string) $value === '1') {
|
||||||
|
return $query->where(function (Builder $q) use ($cutoff): void {
|
||||||
|
$q->whereNull('last_seen_at')
|
||||||
|
->orWhere('last_seen_at', '<', $cutoff);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->where('last_seen_at', '>=', $cutoff);
|
||||||
|
}),
|
||||||
|
|
||||||
|
SelectFilter::make('group_type')
|
||||||
|
->label('Type')
|
||||||
|
->options([
|
||||||
|
'security' => 'Security',
|
||||||
|
'microsoft365' => 'Microsoft 365',
|
||||||
|
'mail' => 'Mail-enabled',
|
||||||
|
'unknown' => 'Unknown',
|
||||||
|
])
|
||||||
|
->query(function (Builder $query, array $data): Builder {
|
||||||
|
$value = (string) ($data['value'] ?? '');
|
||||||
|
|
||||||
|
if ($value === '') {
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
return match ($value) {
|
||||||
|
'microsoft365' => $query->whereJsonContains('group_types', 'Unified'),
|
||||||
|
'security' => $query
|
||||||
|
->where('security_enabled', true)
|
||||||
|
->where(function (Builder $q): void {
|
||||||
|
$q->whereNull('group_types')
|
||||||
|
->orWhereJsonDoesntContain('group_types', 'Unified');
|
||||||
|
}),
|
||||||
|
'mail' => $query
|
||||||
|
->where('mail_enabled', true)
|
||||||
|
->where(function (Builder $q): void {
|
||||||
|
$q->whereNull('group_types')
|
||||||
|
->orWhereJsonDoesntContain('group_types', 'Unified');
|
||||||
|
}),
|
||||||
|
'unknown' => $query
|
||||||
|
->where(function (Builder $q): void {
|
||||||
|
$q->whereNull('group_types')
|
||||||
|
->orWhereJsonDoesntContain('group_types', 'Unified');
|
||||||
|
})
|
||||||
|
->where('security_enabled', false)
|
||||||
|
->where('mail_enabled', false),
|
||||||
|
default => $query,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Action::make('select')
|
||||||
|
->label('Select')
|
||||||
|
->icon('heroicon-o-check')
|
||||||
|
->color('primary')
|
||||||
|
->action(function (EntraGroup $record): void {
|
||||||
|
$this->dispatch('entra-group-cache-picked', sourceGroupId: $this->sourceGroupId, entraId: (string) $record->entra_id);
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
->emptyStateHeading('No cached groups found')
|
||||||
|
->emptyStateDescription('Run “Sync Groups” first, then come back here.')
|
||||||
|
->emptyStateActions([
|
||||||
|
Action::make('open_groups')
|
||||||
|
->label('Directory Groups')
|
||||||
|
->icon('heroicon-o-user-group')
|
||||||
|
->url(fn (): string => EntraGroupResource::getUrl('index', tenant: Tenant::current())),
|
||||||
|
Action::make('open_sync_runs')
|
||||||
|
->label('Group Sync Runs')
|
||||||
|
->icon('heroicon-o-clock')
|
||||||
|
->url(fn (): string => EntraGroupSyncRunResource::getUrl('index', tenant: Tenant::current())),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render(): View
|
||||||
|
{
|
||||||
|
return view('livewire.entra-group-cache-picker-table');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function groupType(EntraGroup $record): string
|
||||||
|
{
|
||||||
|
$groupTypes = $record->group_types;
|
||||||
|
|
||||||
|
if (is_array($groupTypes) && in_array('Unified', $groupTypes, true)) {
|
||||||
|
return 'microsoft365';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($record->security_enabled) {
|
||||||
|
return 'security';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($record->mail_enabled) {
|
||||||
|
return 'mail';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function groupTypeLabel(string $type): string
|
||||||
|
{
|
||||||
|
return match ($type) {
|
||||||
|
'microsoft365' => 'Microsoft 365',
|
||||||
|
'security' => 'Security',
|
||||||
|
'mail' => 'Mail-enabled',
|
||||||
|
default => 'Unknown',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function groupTypeColor(string $type): string
|
||||||
|
{
|
||||||
|
return match ($type) {
|
||||||
|
'microsoft365' => 'info',
|
||||||
|
'security' => 'success',
|
||||||
|
'mail' => 'warning',
|
||||||
|
default => 'gray',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -45,6 +45,8 @@ public function createRun(
|
|||||||
array $itemIds,
|
array $itemIds,
|
||||||
int $totalItems
|
int $totalItems
|
||||||
): BulkOperationRun {
|
): BulkOperationRun {
|
||||||
|
$effectiveTotalItems = max($totalItems, count($itemIds));
|
||||||
|
|
||||||
$run = BulkOperationRun::create([
|
$run = BulkOperationRun::create([
|
||||||
'tenant_id' => $tenant->id,
|
'tenant_id' => $tenant->id,
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
@ -52,7 +54,7 @@ public function createRun(
|
|||||||
'action' => $action,
|
'action' => $action,
|
||||||
'status' => 'pending',
|
'status' => 'pending',
|
||||||
'item_ids' => $itemIds,
|
'item_ids' => $itemIds,
|
||||||
'total_items' => $totalItems,
|
'total_items' => $effectiveTotalItems,
|
||||||
'processed_items' => 0,
|
'processed_items' => 0,
|
||||||
'succeeded' => 0,
|
'succeeded' => 0,
|
||||||
'failed' => 0,
|
'failed' => 0,
|
||||||
@ -66,7 +68,7 @@ public function createRun(
|
|||||||
context: [
|
context: [
|
||||||
'metadata' => [
|
'metadata' => [
|
||||||
'bulk_run_id' => $run->id,
|
'bulk_run_id' => $run->id,
|
||||||
'total_items' => $totalItems,
|
'total_items' => $effectiveTotalItems,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
actorId: $user->id,
|
actorId: $user->id,
|
||||||
@ -139,6 +141,14 @@ public function complete(BulkOperationRun $run): void
|
|||||||
{
|
{
|
||||||
$run->refresh();
|
$run->refresh();
|
||||||
|
|
||||||
|
if ($run->processed_items > $run->total_items) {
|
||||||
|
BulkOperationRun::query()
|
||||||
|
->whereKey($run->id)
|
||||||
|
->update(['total_items' => $run->processed_items]);
|
||||||
|
|
||||||
|
$run->refresh();
|
||||||
|
}
|
||||||
|
|
||||||
if (! in_array($run->status, ['pending', 'running'], true)) {
|
if (! in_array($run->status, ['pending', 'running'], true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
<div class="space-y-4">
|
||||||
|
<livewire:entra-group-cache-picker-table :sourceGroupId="$sourceGroupId" />
|
||||||
|
</div>
|
||||||
@ -3,6 +3,8 @@
|
|||||||
@if($runs->isNotEmpty())
|
@if($runs->isNotEmpty())
|
||||||
<div class="fixed bottom-4 right-4 z-[999999] w-96 space-y-2" style="pointer-events: auto;">
|
<div class="fixed bottom-4 right-4 z-[999999] w-96 space-y-2" style="pointer-events: auto;">
|
||||||
@foreach ($runs as $run)
|
@foreach ($runs as $run)
|
||||||
|
@php($effectiveTotal = max((int) $run->total_items, (int) $run->processed_items))
|
||||||
|
@php($percent = $effectiveTotal > 0 ? min(100, round(($run->processed_items / $effectiveTotal) * 100)) : 0)
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl border-2 border-primary-500 dark:border-primary-400 p-4 transition-all animate-in slide-in-from-right duration-300"
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl border-2 border-primary-500 dark:border-primary-400 p-4 transition-all animate-in slide-in-from-right duration-300"
|
||||||
wire:key="run-{{ $run->id }}">
|
wire:key="run-{{ $run->id }}">
|
||||||
|
|
||||||
@ -38,17 +40,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">
|
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||||
{{ $run->processed_items }} / {{ $run->total_items }}
|
{{ $run->processed_items }} / {{ $effectiveTotal }}
|
||||||
</span>
|
</span>
|
||||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||||
{{ $run->total_items > 0 ? round(($run->processed_items / $run->total_items) * 100) : 0 }}%
|
{{ $percent }}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full bg-gray-200 rounded-full h-3 dark:bg-gray-700 overflow-hidden">
|
<div class="w-full bg-gray-200 rounded-full h-3 dark:bg-gray-700 overflow-hidden">
|
||||||
<div class="bg-primary-600 dark:bg-primary-500 h-3 rounded-full transition-all duration-300 ease-out"
|
<div class="bg-primary-600 dark:bg-primary-500 h-3 rounded-full transition-all duration-300 ease-out"
|
||||||
style="width: {{ $run->total_items > 0 ? ($run->processed_items / $run->total_items) * 100 : 0 }}%"></div>
|
style="width: {{ $percent }}%"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-2 flex items-center justify-between text-xs">
|
<div class="mt-2 flex items-center justify-between text-xs">
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
<div class="space-y-2">
|
||||||
|
{{ $this->table }}
|
||||||
|
</div>
|
||||||
@ -202,3 +202,32 @@
|
|||||||
'action' => 'restore.group_mapping.applied',
|
'action' => 'restore.group_mapping.applied',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('restore wizard can fill a group mapping entry from directory cache picker', function () {
|
||||||
|
$tenant = Tenant::create([
|
||||||
|
'tenant_id' => 'tenant-1',
|
||||||
|
'name' => 'Tenant One',
|
||||||
|
'metadata' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
|
$sourceGroupId = fake()->uuid();
|
||||||
|
$targetGroupId = fake()->uuid();
|
||||||
|
|
||||||
|
Livewire::test(CreateRestoreRun::class)
|
||||||
|
->set('data.check_summary', 'old')
|
||||||
|
->set('data.check_results', ['x'])
|
||||||
|
->call('applyEntraGroupCachePick', sourceGroupId: $sourceGroupId, entraId: $targetGroupId)
|
||||||
|
->assertSet("data.group_mapping.{$sourceGroupId}", $targetGroupId)
|
||||||
|
->assertSet('data.check_summary', null)
|
||||||
|
->assertSet('data.check_results', []);
|
||||||
|
});
|
||||||
|
|||||||
@ -29,3 +29,33 @@
|
|||||||
->and($run->failures)->toBeArray()
|
->and($run->failures)->toBeArray()
|
||||||
->and($run->failures)->toHaveCount(1);
|
->and($run->failures)->toHaveCount(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('bulk operation run total_items is at least the item_ids count', function () {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$service = app(BulkOperationService::class);
|
||||||
|
$run = $service->createRun($tenant, $user, 'inventory', 'sync', ['a', 'b', 'c', 'd'], 1);
|
||||||
|
|
||||||
|
expect($run->total_items)->toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('bulk operation completion clamps total_items up to processed_items', function () {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$service = app(BulkOperationService::class);
|
||||||
|
$run = $service->createRun($tenant, $user, 'inventory', 'sync', ['only-one'], 1);
|
||||||
|
|
||||||
|
$service->start($run);
|
||||||
|
$service->recordSuccess($run);
|
||||||
|
$service->recordSuccess($run);
|
||||||
|
|
||||||
|
$run->refresh();
|
||||||
|
expect($run->processed_items)->toBe(2)->and($run->total_items)->toBe(1);
|
||||||
|
|
||||||
|
$service->complete($run);
|
||||||
|
$run->refresh();
|
||||||
|
|
||||||
|
expect($run->total_items)->toBe(2);
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user