777 lines
32 KiB
PHP
777 lines
32 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Resources;
|
|
|
|
use App\Exceptions\InvalidPolicyTypeException;
|
|
use App\Filament\Resources\BackupScheduleResource\Pages;
|
|
use App\Filament\Resources\BackupScheduleResource\RelationManagers\BackupScheduleRunsRelationManager;
|
|
use App\Jobs\RunBackupScheduleJob;
|
|
use App\Models\BackupSchedule;
|
|
use App\Models\BackupScheduleRun;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Notifications\BackupScheduleRunDispatchedNotification;
|
|
use App\Rules\SupportedPolicyTypesRule;
|
|
use App\Services\BackupScheduling\PolicyTypeResolver;
|
|
use App\Services\BackupScheduling\ScheduleTimeService;
|
|
use App\Services\Intune\AuditLogger;
|
|
use App\Support\TenantRole;
|
|
use BackedEnum;
|
|
use Carbon\CarbonImmutable;
|
|
use DateTimeZone;
|
|
use Filament\Actions\Action;
|
|
use Filament\Actions\ActionGroup;
|
|
use Filament\Actions\BulkAction;
|
|
use Filament\Actions\BulkActionGroup;
|
|
use Filament\Actions\DeleteAction;
|
|
use Filament\Actions\DeleteBulkAction;
|
|
use Filament\Actions\EditAction;
|
|
use Filament\Forms\Components\CheckboxList;
|
|
use Filament\Forms\Components\Select;
|
|
use Filament\Forms\Components\TextInput;
|
|
use Filament\Forms\Components\Toggle;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Resources\Resource;
|
|
use Filament\Schemas\Components\Utilities\Get;
|
|
use Filament\Schemas\Schema;
|
|
use Filament\Tables\Columns\IconColumn;
|
|
use Filament\Tables\Columns\TextColumn;
|
|
use Filament\Tables\Filters\SelectFilter;
|
|
use Filament\Tables\Table;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\UniqueConstraintViolationException;
|
|
use Illuminate\Support\Facades\Bus;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
use UnitEnum;
|
|
|
|
class BackupScheduleResource extends Resource
|
|
{
|
|
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-clock';
|
|
|
|
protected static string|UnitEnum|null $navigationGroup = 'Backups & Restore';
|
|
|
|
protected static function currentTenantRole(): ?TenantRole
|
|
{
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
return null;
|
|
}
|
|
|
|
return $user->tenantRole(Tenant::current());
|
|
}
|
|
|
|
public static function canViewAny(): bool
|
|
{
|
|
return static::currentTenantRole() !== null;
|
|
}
|
|
|
|
public static function canView(Model $record): bool
|
|
{
|
|
return static::currentTenantRole() !== null;
|
|
}
|
|
|
|
public static function canCreate(): bool
|
|
{
|
|
return static::currentTenantRole()?->canManageBackupSchedules() ?? false;
|
|
}
|
|
|
|
public static function canEdit(Model $record): bool
|
|
{
|
|
return static::currentTenantRole()?->canManageBackupSchedules() ?? false;
|
|
}
|
|
|
|
public static function canDelete(Model $record): bool
|
|
{
|
|
return static::currentTenantRole()?->canManageBackupSchedules() ?? false;
|
|
}
|
|
|
|
public static function canDeleteAny(): bool
|
|
{
|
|
return static::currentTenantRole()?->canManageBackupSchedules() ?? false;
|
|
}
|
|
|
|
public static function form(Schema $schema): Schema
|
|
{
|
|
return $schema
|
|
->schema([
|
|
TextInput::make('name')
|
|
->label('Schedule Name')
|
|
->required()
|
|
->maxLength(255),
|
|
|
|
Toggle::make('is_enabled')
|
|
->label('Enabled')
|
|
->default(true),
|
|
|
|
Select::make('timezone')
|
|
->label('Timezone')
|
|
->options(static::timezoneOptions())
|
|
->searchable()
|
|
->default('UTC')
|
|
->required(),
|
|
|
|
Select::make('frequency')
|
|
->label('Frequency')
|
|
->options([
|
|
'daily' => 'Daily',
|
|
'weekly' => 'Weekly',
|
|
])
|
|
->default('daily')
|
|
->required()
|
|
->reactive(),
|
|
|
|
TextInput::make('time_of_day')
|
|
->label('Time of day')
|
|
->type('time')
|
|
->required()
|
|
->extraInputAttributes(['step' => 60]),
|
|
|
|
CheckboxList::make('days_of_week')
|
|
->label('Days of the week')
|
|
->options(static::dayOfWeekOptions())
|
|
->columns(2)
|
|
->visible(fn (Get $get): bool => $get('frequency') === 'weekly')
|
|
->required(fn (Get $get): bool => $get('frequency') === 'weekly')
|
|
->rules(['array', 'min:1']),
|
|
|
|
CheckboxList::make('policy_types')
|
|
->label('Policy types')
|
|
->options(static::policyTypeOptions())
|
|
->columns(2)
|
|
->required()
|
|
->helperText('Select the Microsoft Graph policy types that should be included in each run.')
|
|
->rules([
|
|
'array',
|
|
'min:1',
|
|
new SupportedPolicyTypesRule,
|
|
])
|
|
->columnSpanFull(),
|
|
|
|
Toggle::make('include_foundations')
|
|
->label('Include foundation types')
|
|
->default(true),
|
|
|
|
TextInput::make('retention_keep_last')
|
|
->label('Retention (keep last N Backup Sets)')
|
|
->type('number')
|
|
->default(30)
|
|
->minValue(1)
|
|
->required(),
|
|
]);
|
|
}
|
|
|
|
public static function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->defaultSort('next_run_at', 'asc')
|
|
->columns([
|
|
IconColumn::make('is_enabled')
|
|
->label('Enabled')
|
|
->boolean()
|
|
->alignCenter(),
|
|
|
|
TextColumn::make('name')
|
|
->searchable()
|
|
->label('Schedule'),
|
|
|
|
TextColumn::make('frequency')
|
|
->label('Frequency')
|
|
->badge()
|
|
->formatStateUsing(fn (?string $state): string => match ($state) {
|
|
'daily' => 'Daily',
|
|
'weekly' => 'Weekly',
|
|
default => (string) $state,
|
|
})
|
|
->color(fn (?string $state): string => match ($state) {
|
|
'daily' => 'success',
|
|
'weekly' => 'warning',
|
|
default => 'gray',
|
|
}),
|
|
|
|
TextColumn::make('time_of_day')
|
|
->label('Time')
|
|
->formatStateUsing(fn (?string $state): ?string => $state ? trim($state) : null),
|
|
|
|
TextColumn::make('timezone')
|
|
->label('Timezone'),
|
|
|
|
TextColumn::make('policy_types')
|
|
->label('Policy types')
|
|
->wrap()
|
|
->getStateUsing(function (BackupSchedule $record): string {
|
|
$state = $record->policy_types;
|
|
|
|
if (is_string($state)) {
|
|
$decoded = json_decode($state, true);
|
|
|
|
if (is_array($decoded)) {
|
|
$state = $decoded;
|
|
}
|
|
}
|
|
|
|
if ($state instanceof \Illuminate\Contracts\Support\Arrayable) {
|
|
$state = $state->toArray();
|
|
}
|
|
|
|
if (! is_array($state)) {
|
|
return 'None';
|
|
}
|
|
|
|
$types = array_is_list($state)
|
|
? $state
|
|
: array_keys(array_filter($state));
|
|
|
|
$types = array_values(array_filter($types, fn (mixed $type): bool => is_string($type) && $type !== ''));
|
|
|
|
if ($types === []) {
|
|
return 'None';
|
|
}
|
|
|
|
$labelMap = collect(config('tenantpilot.supported_policy_types', []))
|
|
->mapWithKeys(fn (array $policy): array => [
|
|
(string) ($policy['type'] ?? '') => (string) ($policy['label'] ?? Str::headline((string) ($policy['type'] ?? ''))),
|
|
])
|
|
->filter(fn (string $label, string $type): bool => $type !== '')
|
|
->all();
|
|
|
|
$labels = array_map(
|
|
fn (string $type): string => $labelMap[$type] ?? Str::headline($type),
|
|
$types,
|
|
);
|
|
|
|
return implode(', ', $labels);
|
|
}),
|
|
|
|
TextColumn::make('retention_keep_last')
|
|
->label('Retention')
|
|
->suffix(' sets'),
|
|
|
|
TextColumn::make('last_run_status')
|
|
->label('Last run status')
|
|
->badge()
|
|
->formatStateUsing(fn (?string $state): string => match ($state) {
|
|
BackupScheduleRun::STATUS_RUNNING => 'Running',
|
|
BackupScheduleRun::STATUS_SUCCESS => 'Success',
|
|
BackupScheduleRun::STATUS_PARTIAL => 'Partial',
|
|
BackupScheduleRun::STATUS_FAILED => 'Failed',
|
|
BackupScheduleRun::STATUS_CANCELED => 'Canceled',
|
|
BackupScheduleRun::STATUS_SKIPPED => 'Skipped',
|
|
default => $state ? Str::headline($state) : '—',
|
|
})
|
|
->color(fn (?string $state): string => match ($state) {
|
|
BackupScheduleRun::STATUS_SUCCESS => 'success',
|
|
BackupScheduleRun::STATUS_PARTIAL => 'warning',
|
|
BackupScheduleRun::STATUS_RUNNING => 'primary',
|
|
BackupScheduleRun::STATUS_SKIPPED => 'gray',
|
|
BackupScheduleRun::STATUS_FAILED,
|
|
BackupScheduleRun::STATUS_CANCELED => 'danger',
|
|
default => 'gray',
|
|
}),
|
|
|
|
TextColumn::make('last_run_at')
|
|
->label('Last run')
|
|
->dateTime()
|
|
->sortable(),
|
|
|
|
TextColumn::make('next_run_at')
|
|
->label('Next run')
|
|
->getStateUsing(function (BackupSchedule $record): ?string {
|
|
$nextRun = $record->next_run_at;
|
|
|
|
if (! $nextRun) {
|
|
return null;
|
|
}
|
|
|
|
$timezone = $record->timezone ?: 'UTC';
|
|
|
|
try {
|
|
return $nextRun->setTimezone($timezone)->format('M j, Y H:i:s');
|
|
} catch (\Throwable) {
|
|
return $nextRun->format('M j, Y H:i:s');
|
|
}
|
|
})
|
|
->sortable(),
|
|
])
|
|
->filters([
|
|
SelectFilter::make('enabled_state')
|
|
->label('Enabled')
|
|
->options([
|
|
'enabled' => 'Enabled',
|
|
'disabled' => 'Disabled',
|
|
])
|
|
->query(function (Builder $query, array $data): void {
|
|
$value = $data['value'] ?? null;
|
|
|
|
if (blank($value)) {
|
|
return;
|
|
}
|
|
|
|
if ($value === 'enabled') {
|
|
$query->where('is_enabled', true);
|
|
|
|
return;
|
|
}
|
|
|
|
if ($value === 'disabled') {
|
|
$query->where('is_enabled', false);
|
|
}
|
|
}),
|
|
])
|
|
->actions([
|
|
ActionGroup::make([
|
|
Action::make('runNow')
|
|
->label('Run now')
|
|
->icon('heroicon-o-play')
|
|
->color('success')
|
|
->visible(fn (): bool => static::currentTenantRole()?->canRunBackupSchedules() ?? false)
|
|
->action(function (BackupSchedule $record): void {
|
|
abort_unless(static::currentTenantRole()?->canRunBackupSchedules() ?? false, 403);
|
|
|
|
$tenant = Tenant::current();
|
|
$user = auth()->user();
|
|
|
|
$scheduledFor = CarbonImmutable::now('UTC')->startOfMinute();
|
|
$run = null;
|
|
|
|
for ($i = 0; $i < 5; $i++) {
|
|
try {
|
|
$run = BackupScheduleRun::create([
|
|
'backup_schedule_id' => $record->id,
|
|
'tenant_id' => $tenant->getKey(),
|
|
'scheduled_for' => $scheduledFor->toDateTimeString(),
|
|
'status' => BackupScheduleRun::STATUS_RUNNING,
|
|
'summary' => null,
|
|
]);
|
|
break;
|
|
} catch (UniqueConstraintViolationException) {
|
|
$scheduledFor = $scheduledFor->addMinute();
|
|
}
|
|
}
|
|
|
|
if (! $run instanceof BackupScheduleRun) {
|
|
Notification::make()
|
|
->title('Run already queued')
|
|
->body('Please wait a moment and try again.')
|
|
->warning()
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
app(AuditLogger::class)->log(
|
|
tenant: $tenant,
|
|
action: 'backup_schedule.run_dispatched_manual',
|
|
resourceType: 'backup_schedule_run',
|
|
resourceId: (string) $run->id,
|
|
status: 'success',
|
|
context: [
|
|
'metadata' => [
|
|
'backup_schedule_id' => $record->id,
|
|
'backup_schedule_run_id' => $run->id,
|
|
'scheduled_for' => $scheduledFor->toDateTimeString(),
|
|
'trigger' => 'run_now',
|
|
],
|
|
],
|
|
);
|
|
|
|
Bus::dispatch(new RunBackupScheduleJob($run->id));
|
|
|
|
if ($user instanceof User) {
|
|
$user->notify(new BackupScheduleRunDispatchedNotification([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'backup_schedule_id' => (int) $record->id,
|
|
'backup_schedule_run_id' => (int) $run->id,
|
|
'scheduled_for' => $scheduledFor->toDateTimeString(),
|
|
'trigger' => 'run_now',
|
|
]));
|
|
}
|
|
|
|
Notification::make()
|
|
->title('Run dispatched')
|
|
->body('The backup run has been queued.')
|
|
->success()
|
|
->send();
|
|
}),
|
|
Action::make('retry')
|
|
->label('Retry')
|
|
->icon('heroicon-o-arrow-path')
|
|
->color('warning')
|
|
->visible(fn (): bool => static::currentTenantRole()?->canRunBackupSchedules() ?? false)
|
|
->action(function (BackupSchedule $record): void {
|
|
abort_unless(static::currentTenantRole()?->canRunBackupSchedules() ?? false, 403);
|
|
|
|
$tenant = Tenant::current();
|
|
$user = auth()->user();
|
|
|
|
$scheduledFor = CarbonImmutable::now('UTC')->startOfMinute();
|
|
$run = null;
|
|
|
|
for ($i = 0; $i < 5; $i++) {
|
|
try {
|
|
$run = BackupScheduleRun::create([
|
|
'backup_schedule_id' => $record->id,
|
|
'tenant_id' => $tenant->getKey(),
|
|
'scheduled_for' => $scheduledFor->toDateTimeString(),
|
|
'status' => BackupScheduleRun::STATUS_RUNNING,
|
|
'summary' => null,
|
|
]);
|
|
break;
|
|
} catch (UniqueConstraintViolationException) {
|
|
$scheduledFor = $scheduledFor->addMinute();
|
|
}
|
|
}
|
|
|
|
if (! $run instanceof BackupScheduleRun) {
|
|
Notification::make()
|
|
->title('Retry already queued')
|
|
->body('Please wait a moment and try again.')
|
|
->warning()
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
app(AuditLogger::class)->log(
|
|
tenant: $tenant,
|
|
action: 'backup_schedule.run_dispatched_manual',
|
|
resourceType: 'backup_schedule_run',
|
|
resourceId: (string) $run->id,
|
|
status: 'success',
|
|
context: [
|
|
'metadata' => [
|
|
'backup_schedule_id' => $record->id,
|
|
'backup_schedule_run_id' => $run->id,
|
|
'scheduled_for' => $scheduledFor->toDateTimeString(),
|
|
'trigger' => 'retry',
|
|
],
|
|
],
|
|
);
|
|
|
|
Bus::dispatch(new RunBackupScheduleJob($run->id));
|
|
|
|
if ($user instanceof User) {
|
|
$user->notify(new BackupScheduleRunDispatchedNotification([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'backup_schedule_id' => (int) $record->id,
|
|
'backup_schedule_run_id' => (int) $run->id,
|
|
'scheduled_for' => $scheduledFor->toDateTimeString(),
|
|
'trigger' => 'retry',
|
|
]));
|
|
}
|
|
|
|
Notification::make()
|
|
->title('Retry dispatched')
|
|
->body('A new backup run has been queued.')
|
|
->success()
|
|
->send();
|
|
}),
|
|
EditAction::make()
|
|
->visible(fn (): bool => static::currentTenantRole()?->canManageBackupSchedules() ?? false),
|
|
DeleteAction::make()
|
|
->visible(fn (): bool => static::currentTenantRole()?->canManageBackupSchedules() ?? false),
|
|
])->icon('heroicon-o-ellipsis-vertical'),
|
|
])
|
|
->bulkActions([
|
|
BulkActionGroup::make([
|
|
BulkAction::make('bulk_run_now')
|
|
->label('Run now')
|
|
->icon('heroicon-o-play')
|
|
->color('success')
|
|
->visible(fn (): bool => static::currentTenantRole()?->canRunBackupSchedules() ?? false)
|
|
->action(function (Collection $records): void {
|
|
abort_unless(static::currentTenantRole()?->canRunBackupSchedules() ?? false, 403);
|
|
|
|
if ($records->isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
$tenant = Tenant::current();
|
|
$user = auth()->user();
|
|
|
|
$createdRunIds = [];
|
|
|
|
/** @var BackupSchedule $record */
|
|
foreach ($records as $record) {
|
|
$scheduledFor = CarbonImmutable::now('UTC')->startOfMinute();
|
|
$run = null;
|
|
|
|
for ($i = 0; $i < 5; $i++) {
|
|
try {
|
|
$run = BackupScheduleRun::create([
|
|
'backup_schedule_id' => $record->id,
|
|
'tenant_id' => $tenant->getKey(),
|
|
'scheduled_for' => $scheduledFor->toDateTimeString(),
|
|
'status' => BackupScheduleRun::STATUS_RUNNING,
|
|
'summary' => null,
|
|
]);
|
|
break;
|
|
} catch (UniqueConstraintViolationException) {
|
|
$scheduledFor = $scheduledFor->addMinute();
|
|
}
|
|
}
|
|
|
|
if (! $run instanceof BackupScheduleRun) {
|
|
continue;
|
|
}
|
|
|
|
$createdRunIds[] = (int) $run->id;
|
|
|
|
app(AuditLogger::class)->log(
|
|
tenant: $tenant,
|
|
action: 'backup_schedule.run_dispatched_manual',
|
|
resourceType: 'backup_schedule_run',
|
|
resourceId: (string) $run->id,
|
|
status: 'success',
|
|
context: [
|
|
'metadata' => [
|
|
'backup_schedule_id' => $record->id,
|
|
'backup_schedule_run_id' => $run->id,
|
|
'scheduled_for' => $scheduledFor->toDateTimeString(),
|
|
'trigger' => 'bulk_run_now',
|
|
],
|
|
],
|
|
);
|
|
|
|
Bus::dispatch(new RunBackupScheduleJob($run->id));
|
|
}
|
|
|
|
if ($user instanceof User) {
|
|
$user->notify(new BackupScheduleRunDispatchedNotification([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'schedule_ids' => $records->pluck('id')->map(fn ($id) => (int) $id)->values()->all(),
|
|
'backup_schedule_run_ids' => $createdRunIds,
|
|
'scheduled_for' => CarbonImmutable::now('UTC')->startOfMinute()->toDateTimeString(),
|
|
'trigger' => 'bulk_run_now',
|
|
]));
|
|
}
|
|
|
|
$notification = Notification::make()
|
|
->title('Runs dispatched')
|
|
->body(sprintf('Queued %d run(s).', count($createdRunIds)));
|
|
|
|
if (count($createdRunIds) === 0) {
|
|
$notification->warning();
|
|
} else {
|
|
$notification->success();
|
|
}
|
|
|
|
$notification->send();
|
|
}),
|
|
BulkAction::make('bulk_retry')
|
|
->label('Retry')
|
|
->icon('heroicon-o-arrow-path')
|
|
->color('warning')
|
|
->visible(fn (): bool => static::currentTenantRole()?->canRunBackupSchedules() ?? false)
|
|
->action(function (Collection $records): void {
|
|
abort_unless(static::currentTenantRole()?->canRunBackupSchedules() ?? false, 403);
|
|
|
|
if ($records->isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
$tenant = Tenant::current();
|
|
$user = auth()->user();
|
|
|
|
$createdRunIds = [];
|
|
|
|
/** @var BackupSchedule $record */
|
|
foreach ($records as $record) {
|
|
$scheduledFor = CarbonImmutable::now('UTC')->startOfMinute();
|
|
$run = null;
|
|
|
|
for ($i = 0; $i < 5; $i++) {
|
|
try {
|
|
$run = BackupScheduleRun::create([
|
|
'backup_schedule_id' => $record->id,
|
|
'tenant_id' => $tenant->getKey(),
|
|
'scheduled_for' => $scheduledFor->toDateTimeString(),
|
|
'status' => BackupScheduleRun::STATUS_RUNNING,
|
|
'summary' => null,
|
|
]);
|
|
break;
|
|
} catch (UniqueConstraintViolationException) {
|
|
$scheduledFor = $scheduledFor->addMinute();
|
|
}
|
|
}
|
|
|
|
if (! $run instanceof BackupScheduleRun) {
|
|
continue;
|
|
}
|
|
|
|
$createdRunIds[] = (int) $run->id;
|
|
|
|
app(AuditLogger::class)->log(
|
|
tenant: $tenant,
|
|
action: 'backup_schedule.run_dispatched_manual',
|
|
resourceType: 'backup_schedule_run',
|
|
resourceId: (string) $run->id,
|
|
status: 'success',
|
|
context: [
|
|
'metadata' => [
|
|
'backup_schedule_id' => $record->id,
|
|
'backup_schedule_run_id' => $run->id,
|
|
'scheduled_for' => $scheduledFor->toDateTimeString(),
|
|
'trigger' => 'bulk_retry',
|
|
],
|
|
],
|
|
);
|
|
|
|
Bus::dispatch(new RunBackupScheduleJob($run->id));
|
|
}
|
|
|
|
if ($user instanceof User) {
|
|
$user->notify(new BackupScheduleRunDispatchedNotification([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'schedule_ids' => $records->pluck('id')->map(fn ($id) => (int) $id)->values()->all(),
|
|
'backup_schedule_run_ids' => $createdRunIds,
|
|
'scheduled_for' => CarbonImmutable::now('UTC')->startOfMinute()->toDateTimeString(),
|
|
'trigger' => 'bulk_retry',
|
|
]));
|
|
}
|
|
|
|
$notification = Notification::make()
|
|
->title('Retries dispatched')
|
|
->body(sprintf('Queued %d run(s).', count($createdRunIds)));
|
|
|
|
if (count($createdRunIds) === 0) {
|
|
$notification->warning();
|
|
} else {
|
|
$notification->success();
|
|
}
|
|
|
|
$notification->send();
|
|
}),
|
|
DeleteBulkAction::make('bulk_delete')
|
|
->visible(fn (): bool => static::currentTenantRole()?->canManageBackupSchedules() ?? false),
|
|
]),
|
|
]);
|
|
}
|
|
|
|
public static function getEloquentQuery(): Builder
|
|
{
|
|
$tenantId = Tenant::current()->getKey();
|
|
|
|
return parent::getEloquentQuery()
|
|
->where('tenant_id', $tenantId)
|
|
->orderByDesc('is_enabled')
|
|
->orderBy('next_run_at');
|
|
}
|
|
|
|
public static function getRelations(): array
|
|
{
|
|
return [
|
|
BackupScheduleRunsRelationManager::class,
|
|
];
|
|
}
|
|
|
|
public static function getPages(): array
|
|
{
|
|
return [
|
|
'index' => Pages\ListBackupSchedules::route('/'),
|
|
'create' => Pages\CreateBackupSchedule::route('/create'),
|
|
'edit' => Pages\EditBackupSchedule::route('/{record}/edit'),
|
|
];
|
|
}
|
|
|
|
public static function ensurePolicyTypes(array $data): array
|
|
{
|
|
$types = array_values((array) ($data['policy_types'] ?? []));
|
|
|
|
try {
|
|
app(PolicyTypeResolver::class)->ensureSupported($types);
|
|
} catch (InvalidPolicyTypeException $exception) {
|
|
throw ValidationException::withMessages([
|
|
'policy_types' => [sprintf('Unknown policy types: %s.', implode(', ', $exception->unknownPolicyTypes))],
|
|
]);
|
|
}
|
|
|
|
$data['policy_types'] = $types;
|
|
|
|
return $data;
|
|
}
|
|
|
|
public static function assignTenant(array $data): array
|
|
{
|
|
$data['tenant_id'] = Tenant::current()->getKey();
|
|
|
|
return $data;
|
|
}
|
|
|
|
public static function hydrateNextRun(array $data): array
|
|
{
|
|
if (! empty($data['time_of_day'])) {
|
|
$data['time_of_day'] = static::normalizeTimeOfDay($data['time_of_day']);
|
|
}
|
|
|
|
$schedule = new BackupSchedule;
|
|
$schedule->forceFill([
|
|
'frequency' => $data['frequency'] ?? 'daily',
|
|
'time_of_day' => $data['time_of_day'] ?? '00:00:00',
|
|
'timezone' => $data['timezone'] ?? 'UTC',
|
|
'days_of_week' => (array) ($data['days_of_week'] ?? []),
|
|
]);
|
|
|
|
$nextRun = app(ScheduleTimeService::class)->nextRunFor($schedule);
|
|
|
|
$data['next_run_at'] = $nextRun?->toDateTimeString();
|
|
|
|
return $data;
|
|
}
|
|
|
|
public static function normalizeTimeOfDay(string $time): string
|
|
{
|
|
if (preg_match('/^\d{2}:\d{2}$/', $time)) {
|
|
return $time.':00';
|
|
}
|
|
|
|
return $time;
|
|
}
|
|
|
|
protected static function timezoneOptions(): array
|
|
{
|
|
$zones = DateTimeZone::listIdentifiers();
|
|
|
|
sort($zones);
|
|
|
|
return array_combine($zones, $zones);
|
|
}
|
|
|
|
protected static function policyTypeOptions(): array
|
|
{
|
|
return static::policyTypeLabelMap();
|
|
}
|
|
|
|
protected static function policyTypeLabels(array $types): array
|
|
{
|
|
$map = static::policyTypeLabelMap();
|
|
|
|
return array_map(fn (string $type): string => $map[$type] ?? Str::headline($type), $types);
|
|
}
|
|
|
|
protected static function policyTypeLabelMap(): array
|
|
{
|
|
return collect(config('tenantpilot.supported_policy_types', []))
|
|
->mapWithKeys(fn (array $policy) => [
|
|
$policy['type'] => $policy['label'] ?? Str::headline($policy['type']),
|
|
])
|
|
->all();
|
|
}
|
|
|
|
protected static function dayOfWeekOptions(): array
|
|
{
|
|
return [
|
|
1 => 'Monday',
|
|
2 => 'Tuesday',
|
|
3 => 'Wednesday',
|
|
4 => 'Thursday',
|
|
5 => 'Friday',
|
|
6 => 'Saturday',
|
|
7 => 'Sunday',
|
|
];
|
|
}
|
|
}
|