Merge remote-tracking branch 'origin/next' into v5-parallel-inertia-react

This commit is contained in:
Andras Bacsai committed 2026-07-20 12:09:17 +02:00
commit ee36124300
82 files changed
+8190 -463

No files matched your search

@@ -39,7 +39,8 @@
value={{ $domValue }} id="{{ $htmlId }}" @if ($checked) checked @endif />
@else
<input type="checkbox" @disabled($disabled) {{ $attributes->class([$defaultClass, 'shrink-0']) }}
wire:model={{ $value ?? $modelBinding }} id="{{ $htmlId }}" @if ($checked) checked @endif />
@if ($live) wire:model.live={{ $value ?? $modelBinding }} @else wire:model={{ $value ?? $modelBinding }} @endif
id="{{ $htmlId }}" @if ($checked) checked @endif />
@endif
@endif
</label>
@@ -10,6 +10,9 @@
@if ($helper)
<x-helper :helper="$helper" />
@endif
@isset($labelSuffix)
{{ $labelSuffix }}
@endisset
</label>
@endif
@if ($type === 'password')
@@ -21,7 +21,7 @@
$showUnknownHelper = ! str($status)->startsWith('Proxy') && (str($status)->contains('unknown') || str($healthStatus)->contains('unknown'));
$showUnhealthyHelper = ! str($status)->startsWith('Proxy') && (str($status)->contains('unhealthy') || str($healthStatus)->contains('unhealthy'));
@endphp
<div class="flex items-center gap-1">
<div class="flex items-center gap-1 leading-none">
@if ($lastDeploymentLink)
<x-status-badge as="a" href="{{ $lastDeploymentLink }}" target="_blank" status="{{ $badgeStatus }}" type="success"
title="{{ $title }}" class="cursor-pointer underline" />
@@ -0,0 +1,18 @@
<form class="flex flex-col w-full gap-4 rounded-sm" wire:submit="submit">
@if ($targets->isEmpty())
<div class="text-warning">Add a persistent volume or directory mount before configuring a backup.</div>
@else
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<x-forms.select id="targetKey" label="Backup Target" required :disabled="$targetLocked">
@foreach ($targets as $target)
<option value="{{ $target['key'] }}">{{ $target['type'] }}: {{ $target['name'] }}</option>
@endforeach
</x-forms.select>
<x-forms.input id="frequency" placeholder="0 0 * * * or daily"
helper="Use every_minute, hourly, daily, weekly, monthly, yearly, or a cron expression."
label="Frequency" required />
</div>
<x-forms.button type="submit">Save</x-forms.button>
@endif
</form>
@@ -0,0 +1,89 @@
<div x-data="{
search: @js($search),
backups: @js($backups->map(fn ($backup) => [
'name' => strtolower($backup->targetName()),
'type' => strtolower($backup->targetType()),
'frequency' => strtolower($backup->frequency),
])->values()),
hasMatches() {
const query = this.search.toLowerCase();
return this.backups.some((backup) => backup.name.includes(query) || backup.type.includes(query) || backup.frequency.includes(query));
},
}">
<x-slot:title>
{{ data_get_str($application, 'name')->limit(10) }} > Backups | Coolify
</x-slot>
<h1>Backups</h1>
<livewire:project.shared.configuration-checker :resource="$application" />
<livewire:project.application.heading :application="$application" />
<div class="flex items-center gap-2 pb-4">
<h2>Scheduled Backups</h2>
@can('update', $application)
<x-modal-input buttonTitle="+ Add" title="New Scheduled Backup" :wireIgnore="false">
<livewire:project.application.backup.create :application="$application"
wire:key="create-volume-backup-{{ $application->id }}" />
</x-modal-input>
@endcan
</div>
<div class="max-w-md pb-4">
<x-forms.input id="null" type="search" x-model="search" placeholder="Search by target name, type, or frequency..." />
</div>
<div class="flex flex-col gap-2">
<div x-cloak x-show="search !== '' && backups.length > 0 && !hasMatches()">
No scheduled backups match your search.
</div>
@forelse ($backups as $backup)
@php($latestExecution = $backup->latestExecution)
<a x-show="search === '' || @js(strtolower($backup->targetName())).includes(search.toLowerCase()) || @js(strtolower($backup->targetType())).includes(search.toLowerCase()) || @js(strtolower($backup->frequency)).includes(search.toLowerCase())" @class([
'flex flex-col border-l-2 transition-colors p-4 cursor-pointer bg-white hover:bg-gray-100 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 text-black dark:text-white',
'border-blue-500/50 border-dashed' => $latestExecution?->status === 'running',
'border-error' => $latestExecution?->status === 'failed',
'border-success' => $latestExecution?->status === 'success',
'border-gray-200 dark:border-coolgray-300' => !$latestExecution,
]) {{ wireNavigate() }}
href="{{ route('project.application.backup.show', [...$parameters, 'backup_uuid' => $backup->uuid]) }}">
<div class="flex flex-wrap items-center gap-2 mb-2">
@if ($latestExecution)
<span @class([
'px-3 py-1 rounded-md text-xs font-medium tracking-wide shadow-xs',
'bg-blue-100/80 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300' => $latestExecution->status === 'running',
'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200' => $latestExecution->status === 'failed',
'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200' => $latestExecution->status === 'success',
])>
{{ $latestExecution->status === 'running' ? 'In Progress' : ucfirst($latestExecution->status) }}
</span>
@else
<span class="px-3 py-1 text-xs font-medium tracking-wide text-gray-800 bg-gray-100 rounded-md shadow-xs dark:bg-neutral-800 dark:text-gray-200">
No executions yet
</span>
@endif
<h3 class="font-semibold">{{ $backup->frequency }}</h3>
@if (!$backup->enabled)
<span class="text-xs text-neutral-500">Disabled</span>
@endif
</div>
<div class="text-sm text-gray-600 dark:text-gray-400">
{{ $backup->targetType() }}: {{ $backup->targetName() }}
@if ($latestExecution?->finished_at)
• Last run {{ $latestExecution->finished_at->diffForHumans() }}
@else
• Last run: Never
@endif
• Total executions: {{ $backup->executions_count }}
@if ($backup->save_s3)
• S3: Enabled
@endif
@if (($latestExecution?->size ?? 0) > 0)
• Size: {{ formatBytes($latestExecution->size) }}
@endif
</div>
</a>
@empty
<div>No scheduled backups configured.</div>
@endforelse
</div>
</div>
@@ -0,0 +1,37 @@
<div>
<x-slot:title>
{{ data_get_str($application, 'name')->limit(10) }} > Backups | Coolify
</x-slot>
<h1>Backups</h1>
<livewire:project.shared.configuration-checker :resource="$application" />
<livewire:project.application.heading :application="$application" />
<div class="flex flex-col h-full gap-2 md:gap-8 md:flex-row">
<div class="sub-menu-wrapper">
<a @class(['sub-menu-item', 'menu-item-active' => $section === 'general']) {{ wireNavigate() }}
href="{{ route('project.application.backup.show', $parameters) }}">
<span class="menu-item-label">General</span>
</a>
<a @class(['sub-menu-item', 'menu-item-active' => $section === 's3']) {{ wireNavigate() }}
href="{{ route('project.application.backup.s3', $parameters) }}">
<span class="menu-item-label">S3</span>
</a>
<a @class(['sub-menu-item', 'menu-item-active' => $section === 'retention']) {{ wireNavigate() }}
href="{{ route('project.application.backup.retention', $parameters) }}">
<span class="menu-item-label">Retention</span>
</a>
<a @class(['sub-menu-item', 'menu-item-active' => $section === 'executions']) {{ wireNavigate() }}
href="{{ route('project.application.backup.executions', $parameters) }}">
<span class="menu-item-label">Executions</span>
</a>
<a @class(['sub-menu-item', 'menu-item-active' => $section === 'danger']) {{ wireNavigate() }}
href="{{ route('project.application.backup.danger', $parameters) }}">
<span class="menu-item-label">Danger Zone</span>
</a>
</div>
<div class="w-full md:flex-grow">
<livewire:project.shared.storages.volume-backups :storage="$backup->backupable" :resource="$application"
:section="$section" wire:key="volume-backup-{{ $backup->uuid }}-{{ $section }}" />
</div>
</div>
</div>
@@ -11,6 +11,11 @@
'route' => 'project.application.deployment.index',
'active' => request()->routeIs('project.application.deployment.index', 'project.application.deployment.show'),
],
[
'label' => 'Backups',
'route' => 'project.application.backup.index',
'active' => request()->routeIs('project.application.backup.*'),
],
[
'label' => 'Logs',
'route' => 'project.application.logs',
@@ -447,6 +452,10 @@
href="{{ route('project.application.deployment.index', $parameters) }}">
Deployments
</a>
<a class="hidden md:block shrink-0 {{ request()->routeIs('project.application.backup.*') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('project.application.backup.index', $parameters) }}">
Backups
</a>
<a class="hidden md:block shrink-0 {{ request()->routeIs('project.application.logs') ? 'dark:text-white' : '' }}"
href="{{ route('project.application.logs', $parameters) }}">
<div class="flex items-center gap-1">
@@ -1,155 +1,11 @@
<form wire:submit="submit">
<div class="flex flex-col gap-3 pb-4 sm:flex-row sm:items-center">
<h2>Scheduled Backup</h2>
<div class="flex flex-col gap-2 sm:flex-row sm:flex-wrap">
<x-forms.button type="submit" class="w-full sm:w-auto">
Save
</x-forms.button>
@if (str($status)->startsWith('running'))
<x-forms.button wire:click='backupNow' class="w-full sm:w-auto">Backup Now</x-forms.button>
@endif
@if ($backup->database_id !== 0)
<div class="w-full sm:w-auto">
<x-modal-confirmation title="Confirm Backup Schedule Deletion?" isErrorButton submitAction="delete"
:checkboxes="$checkboxes" :actions="[
'The selected backup schedule will be deleted.',
'Scheduled backups for this database will be stopped (if this is the only backup schedule for this database).',
]"
confirmationText="{{ $backup->database->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Database Name of the scheduled backups below"
shortConfirmationLabel="Database Name">
<x-slot:trigger>
<x-forms.button isError class="w-full sm:w-auto">Delete Backups and Schedule</x-forms.button>
</x-slot:trigger>
</x-modal-confirmation>
</div>
@endif
</div>
</div>
<div class="w-full max-w-md pb-2">
<x-forms.checkbox instantSave label="Backup Enabled" id="backupEnabled" />
@if ($availableS3Storages->count() > 0)
<x-forms.checkbox instantSave label="S3 Enabled" id="saveS3" />
@else
<x-forms.checkbox instantSave helper="No validated S3 storage available." label="S3 Enabled" id="saveS3"
disabled />
@endif
@if ($saveS3)
<x-forms.checkbox instantSave label="Disable Local Backup" id="disableLocalBackup"
helper="When enabled, backup files will be deleted from local storage immediately after uploading to S3. This requires S3 backup to be enabled." />
@else
<x-forms.checkbox disabled label="Disable Local Backup" id="disableLocalBackup"
helper="When enabled, backup files will be deleted from local storage immediately after uploading to S3. This requires S3 backup to be enabled." />
@endif
</div>
<div class="w-full max-w-md pb-6">
<div class="flex gap-1 items-center mb-1 text-sm font-medium">
<span>S3 Storage</span>
@if (!$saveS3)
<span class="text-xs font-normal text-warning">(currently disabled)</span>
@endif
@if ($saveS3)
<x-highlighted text="*" />
@endif
</div>
<x-forms.select id="s3StorageId" wire:model.live="s3StorageId" :required="$saveS3"
:disabled="$availableS3Storages->isEmpty()">
@if ($availableS3Storages->isEmpty())
<option value="">No S3 storage available</option>
@else
@foreach ($availableS3Storages as $s3)
<option value="{{ $s3->id }}">{{ $s3->name }}</option>
@endforeach
@endif
</x-forms.select>
</div>
<div class="flex flex-col gap-2">
<h3>Settings</h3>
<div class="flex gap-2 flex-col ">
@if ($backup->database_type === 'App\Models\StandalonePostgresql' && $backup->database_id !== 0)
<div class="w-48">
<x-forms.checkbox label="Backup All Databases" id="dumpAll" instantSave />
</div>
@if (!$backup->dump_all)
<x-forms.input label="Databases To Backup"
helper="Comma separated list of databases to backup. Empty will include the default one."
id="databasesToBackup" />
@endif
@elseif($backup->database_type === 'App\Models\StandaloneMongodb')
<x-forms.input label="Databases To Include"
helper="A list of databases to backup. You can specify which collection(s) per database to exclude from the backup. Empty will include all databases and collections.<br><br>Example:<br><br>database1:collection1,collection2|database2:collection3,collection4<br><br> database1 will include all collections except collection1 and collection2. <br>database2 will include all collections except collection3 and collection4.<br><br>Another Example:<br><br>database1:collection1|database2<br><br> database1 will include all collections except collection1.<br>database2 will include ALL collections."
id="databasesToBackup" />
@elseif($backup->database_type === 'App\Models\StandaloneMysql')
<div class="w-48">
<x-forms.checkbox label="Backup All Databases" id="dumpAll" instantSave />
</div>
@if (!$backup->dump_all)
<x-forms.input label="Databases To Backup"
helper="Comma separated list of databases to backup. Empty will include the default one."
id="databasesToBackup" />
@endif
@elseif($backup->database_type === 'App\Models\StandaloneMariadb')
<div class="w-48">
<x-forms.checkbox label="Backup All Databases" id="dumpAll" instantSave />
</div>
@if (!$backup->dump_all)
<x-forms.input label="Databases To Backup"
helper="Comma separated list of databases to backup. Empty will include the default one."
id="databasesToBackup" />
@endif
@elseif($backup->database_type === 'App\Models\StandaloneClickhouse')
<x-forms.input label="Databases To Backup"
helper="Comma separated list of databases to backup. Empty will include the default one."
id="databasesToBackup" />
@endif
</div>
<div class="grid grid-cols-1 gap-2 md:grid-cols-3">
<x-forms.input label="Frequency" id="frequency" required />
<x-forms.input label="Timezone" id="timezone" disabled
helper="The timezone of the server where the backup is scheduled to run (if not set, the instance timezone will be used)" required />
<x-forms.input label="Timeout" id="timeout" type="number" min="60" helper="The timeout of the backup job in seconds." required />
</div>
<h3 class="mt-6 mb-2 text-lg font-medium">Backup Retention Settings</h3>
<div class="mb-4">
<ul class="list-disc pl-6 space-y-2">
<li>Setting a value to 0 means unlimited retention.</li>
<li>The retention rules work independently - whichever limit is reached first will trigger cleanup.</li>
</ul>
</div>
<div class="flex gap-6 flex-col">
<div>
<h4 class="mb-3 font-medium">Local Backup Retention</h4>
<div class="grid grid-cols-1 gap-2 md:grid-cols-3">
<x-forms.input label="Number of backups to keep" id="databaseBackupRetentionAmountLocally"
type="number" min="0"
helper="Keeps only the specified number of most recent backups on the server. Set to 0 for unlimited backups." required />
<x-forms.input label="Days to keep backups" id="databaseBackupRetentionDaysLocally" type="number"
min="0"
helper="Automatically removes backups older than the specified number of days. Set to 0 for no time limit." required />
<x-forms.input label="Maximum storage (GB)" id="databaseBackupRetentionMaxStorageLocally"
type="number" min="0" step="any"
helper="When total size of all backups in the current backup job exceeds this limit in GB, the oldest backups will be removed. Decimal values are supported (e.g. 0.001 for 1MB). Set to 0 for unlimited storage." required />
</div>
</div>
@if ($saveS3)
<div>
<h4 class="mb-3 font-medium">S3 Storage Retention</h4>
<div class="grid grid-cols-1 gap-2 md:grid-cols-3">
<x-forms.input label="Number of backups to keep" id="databaseBackupRetentionAmountS3"
type="number" min="0"
helper="Keeps only the specified number of most recent backups on S3 storage. Set to 0 for unlimited backups." required />
<x-forms.input label="Days to keep backups" id="databaseBackupRetentionDaysS3" type="number"
min="0"
helper="Automatically removes S3 backups older than the specified number of days. Set to 0 for no time limit." required />
<x-forms.input label="Maximum storage (GB)" id="databaseBackupRetentionMaxStorageS3"
type="number" min="0" step="any"
helper="When total size of all backups in the current backup job exceeds this limit in GB, the oldest backups will be removed. Decimal values are supported (e.g. 0.5 for 500MB). Set to 0 for unlimited storage." required />
</div>
</div>
@endif
</div>
</div>
</form>
<div>
@if ($section === 'retention')
@include('livewire.project.database.backup-edit.retention')
@elseif ($section === 's3')
@include('livewire.project.database.backup-edit.s3')
@elseif ($section === 'danger')
@include('livewire.project.database.backup-edit.danger')
@else
@include('livewire.project.database.backup-edit.general')
@endif
</div>
@@ -0,0 +1,27 @@
<div class="flex flex-col gap-6">
<div>
<h2>Danger Zone</h2>
<div class="text-neutral-500">Woah. I hope you know what you are doing.</div>
</div>
<div>
<h3>Delete Scheduled Backup</h3>
<div class="pb-4 text-neutral-500">
This permanently deletes the schedule. You can also delete its associated backup archives. There is no coming back.
</div>
@if ($backup->database_id !== 0)
<x-modal-confirmation title="Confirm Backup Schedule Deletion?" isErrorButton submitAction="delete"
:checkboxes="$checkboxes" :actions="[
'The selected backup schedule will be deleted.',
'Scheduled backups for this database will be stopped (if this is the only backup schedule for this database).',
]"
confirmationText="{{ $backup->database->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Database Name of the scheduled backups below"
shortConfirmationLabel="Database Name">
<x-slot:trigger>
<x-forms.button isError>Delete Backups and Schedule</x-forms.button>
</x-slot:trigger>
</x-modal-confirmation>
@endif
</div>
</div>
@@ -0,0 +1,66 @@
<form wire:submit="submit" class="flex flex-col gap-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center">
<h2>General</h2>
<div class="flex flex-col gap-2 sm:flex-row sm:flex-wrap">
<x-forms.button type="submit" class="w-full sm:w-auto">Save</x-forms.button>
@if (!$backupEnabled)
<x-forms.button type="button" wire:click="toggleEnabled" wire:loading.attr="disabled"
wire:target="toggleEnabled" isHighlighted>Enable Backup</x-forms.button>
@else
<x-forms.button type="button" wire:click="toggleEnabled" wire:loading.attr="disabled"
wire:target="toggleEnabled">Disable Backup</x-forms.button>
@endif
@if (str($status)->startsWith('running'))
<x-forms.button wire:click="backupNow" class="w-full sm:w-auto">Backup Now</x-forms.button>
@endif
</div>
</div>
<div class="flex flex-col gap-2">
<div class="flex flex-col gap-2">
@if ($backup->database_type === 'App\Models\StandalonePostgresql' && $backup->database_id !== 0)
<div class="w-48">
<x-forms.checkbox label="Backup All Databases" id="dumpAll" instantSave />
</div>
@if (!$backup->dump_all)
<x-forms.input label="Databases To Backup"
helper="Comma separated list of databases to backup. Empty will include the default one."
id="databasesToBackup" />
@endif
@elseif ($backup->database_type === 'App\Models\StandaloneMongodb')
<x-forms.input label="Databases To Include"
helper="A list of databases to backup. You can specify which collection(s) per database to exclude from the backup. Empty will include all databases and collections.<br><br>Example:<br><br>database1:collection1,collection2|database2:collection3,collection4<br><br> database1 will include all collections except collection1 and collection2. <br>database2 will include all collections except collection3 and collection4.<br><br>Another Example:<br><br>database1:collection1|database2<br><br> database1 will include all collections except collection1.<br>database2 will include ALL collections."
id="databasesToBackup" />
@elseif ($backup->database_type === 'App\Models\StandaloneMysql')
<div class="w-48">
<x-forms.checkbox label="Backup All Databases" id="dumpAll" instantSave />
</div>
@if (!$backup->dump_all)
<x-forms.input label="Databases To Backup"
helper="Comma separated list of databases to backup. Empty will include the default one."
id="databasesToBackup" />
@endif
@elseif ($backup->database_type === 'App\Models\StandaloneMariadb')
<div class="w-48">
<x-forms.checkbox label="Backup All Databases" id="dumpAll" instantSave />
</div>
@if (!$backup->dump_all)
<x-forms.input label="Databases To Backup"
helper="Comma separated list of databases to backup. Empty will include the default one."
id="databasesToBackup" />
@endif
@elseif ($backup->database_type === 'App\Models\StandaloneClickhouse')
<x-forms.input label="Databases To Backup"
helper="Comma separated list of databases to backup. Empty will include the default one."
id="databasesToBackup" />
@endif
</div>
<div class="grid grid-cols-1 gap-2 md:grid-cols-3">
<x-forms.input label="Frequency" id="frequency" required />
<x-forms.input label="Timezone" id="timezone" disabled
helper="The timezone of the server where the backup is scheduled to run (if not set, the instance timezone will be used)" required />
<x-forms.input label="Timeout" id="timeout" type="number" min="60"
helper="The timeout of the backup job in seconds." required />
</div>
</div>
</form>
@@ -0,0 +1,43 @@
<form wire:submit="submit" class="flex flex-col gap-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center">
<h2>Retention</h2>
<x-forms.button type="submit" class="w-full sm:w-auto">Save</x-forms.button>
</div>
<ul class="pl-6 space-y-2 list-disc">
<li>Setting a value to 0 means unlimited retention.</li>
<li>The retention rules work independently - whichever limit is reached first will trigger cleanup.</li>
</ul>
<div class="flex flex-col gap-6">
<div>
<h3 class="mb-3">Local Backup Retention</h3>
<div class="grid grid-cols-1 gap-2 md:grid-cols-3">
<x-forms.input label="Number of backups to keep" id="databaseBackupRetentionAmountLocally"
type="number" min="0"
helper="Keeps only the specified number of most recent backups on the server. Set to 0 for unlimited backups." required />
<x-forms.input label="Days to keep backups" id="databaseBackupRetentionDaysLocally" type="number"
min="0"
helper="Automatically removes backups older than the specified number of days. Set to 0 for no time limit." required />
<x-forms.input label="Maximum storage (GB)" id="databaseBackupRetentionMaxStorageLocally"
type="number" min="0" step="any"
helper="When total size of all backups in the current backup job exceeds this limit in GB, the oldest backups will be removed. Decimal values are supported (e.g. 0.001 for 1MB). Set to 0 for unlimited storage." required />
</div>
</div>
<div>
<h3 class="mb-3">S3 Storage Retention</h3>
<div class="grid grid-cols-1 gap-2 md:grid-cols-3">
<x-forms.input label="Number of backups to keep" id="databaseBackupRetentionAmountS3" type="number"
min="0"
helper="Keeps only the specified number of most recent backups on S3 storage. Set to 0 for unlimited backups." required />
<x-forms.input label="Days to keep backups" id="databaseBackupRetentionDaysS3" type="number"
min="0"
helper="Automatically removes S3 backups older than the specified number of days. Set to 0 for no time limit." required />
<x-forms.input label="Maximum storage (GB)" id="databaseBackupRetentionMaxStorageS3"
type="number" min="0" step="any"
helper="When total size of all backups in the current backup job exceeds this limit in GB, the oldest backups will be removed. Decimal values are supported (e.g. 0.5 for 500MB). Set to 0 for unlimited storage." required />
</div>
</div>
</div>
</form>
@@ -0,0 +1,50 @@
@if ($availableS3Storages->isEmpty())
<div class="flex flex-col gap-4">
<h2>S3</h2>
<div>
No validated S3 available. Configure one <a class="underline dark:text-white" {{ wireNavigate() }}
href="{{ route('storage.index') }}">here</a>.
</div>
</div>
@else
<form wire:submit="submit" class="flex flex-col gap-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center">
<h2>S3</h2>
<x-forms.button type="submit" class="w-full sm:w-auto">Save</x-forms.button>
@if (!$saveS3)
<x-forms.button type="button" wire:click="toggleS3" wire:loading.attr="disabled"
wire:target="toggleS3" isHighlighted>Enable S3</x-forms.button>
@else
<x-forms.button type="button" wire:click="toggleS3" wire:loading.attr="disabled"
wire:target="toggleS3">Disable S3</x-forms.button>
@endif
</div>
<div class="w-full max-w-md pb-2">
<div class="flex items-center gap-1 mb-1 text-sm font-medium">
<span>S3 Storage</span>
@if (!$saveS3)
<span class="text-xs font-normal text-warning">(currently disabled)</span>
@else
<x-highlighted text="*" />
@endif
</div>
<x-forms.select id="s3StorageId" wire:model.live="s3StorageId" :required="$saveS3">
@foreach ($availableS3Storages as $s3)
<option value="{{ $s3->id }}">{{ $s3->name }}</option>
@endforeach
</x-forms.select>
</div>
<div class="w-full max-w-md">
@if ($saveS3)
<x-forms.checkbox instantSave label="Disable Local Backup" id="disableLocalBackup"
helper="When enabled, backup files will be deleted from local storage immediately after uploading to S3. This requires S3 backup to be enabled." />
@else
<x-forms.checkbox disabled label="Disable Local Backup" id="disableLocalBackup"
helper="When enabled, backup files will be deleted from local storage immediately after uploading to S3. This requires S3 backup to be enabled." />
@endif
</div>
</form>
@endif
@@ -1,7 +1,7 @@
<div wire:init='refreshBackupExecutions'>
@isset($backup)
<div class="flex flex-col gap-3 py-4 sm:flex-row sm:flex-wrap sm:items-center">
<h3 class="py-0">Executions <span class="text-xs">({{ $executions_count }})</span></h3>
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
<h2 class="py-0">Executions</h2>
@if ($executions_count > 0)
<div class="flex items-center gap-2">
<x-forms.button disabled="{{ !$showPrev }}" wire:click="previousPage('{{ $defaultTake }}')">
@@ -36,7 +36,7 @@
</div>
</div>
<div @if (!$skip) wire:poll.5000ms="refreshBackupExecutions" @endif
class="flex flex-col gap-4">
class="flex flex-col gap-4 pt-2">
@forelse($executions as $execution)
<div wire:key="{{ data_get($execution, 'id') }}" @class([
'flex flex-col border-l-2 transition-colors p-4 bg-white dark:bg-coolgray-100 text-black dark:text-white',
@@ -5,8 +5,38 @@
<h1>Backups</h1>
<livewire:project.shared.configuration-checker :resource="$database" />
<livewire:project.database.heading :database="$database" />
<div>
<livewire:project.database.backup-edit :backup="$backup" :available-s3-storages="$s3s" :status="data_get($database, 'status')" />
<livewire:project.database.backup-executions :backup="$backup" />
<div class="flex flex-col h-full gap-2 md:gap-8 md:flex-row">
<div class="sub-menu-wrapper">
<a @class(['sub-menu-item', 'menu-item-active' => $section === 'general']) {{ wireNavigate() }}
href="{{ route('project.database.backup.execution', $parameters) }}">
<span class="menu-item-label">General</span>
</a>
<a @class(['sub-menu-item', 'menu-item-active' => $section === 's3']) {{ wireNavigate() }}
href="{{ route('project.database.backup.s3', $parameters) }}">
<span class="menu-item-label">S3</span>
</a>
<a @class(['sub-menu-item', 'menu-item-active' => $section === 'retention']) {{ wireNavigate() }}
href="{{ route('project.database.backup.retention', $parameters) }}">
<span class="menu-item-label">Retention</span>
</a>
<a @class(['sub-menu-item', 'menu-item-active' => $section === 'executions']) {{ wireNavigate() }}
href="{{ route('project.database.backup.executions', $parameters) }}">
<span class="menu-item-label">Executions</span>
</a>
<a @class(['sub-menu-item', 'menu-item-active' => $section === 'danger']) {{ wireNavigate() }}
href="{{ route('project.database.backup.danger', $parameters) }}">
<span class="menu-item-label">Danger Zone</span>
</a>
</div>
<div class="w-full md:flex-grow">
@if ($section === 'executions')
<livewire:project.database.backup-executions :backup="$backup" />
@else
<livewire:project.database.backup-edit :backup="$backup" :available-s3-storages="$s3s"
:status="data_get($database, 'status')" :section="$section"
wire:key="database-backup-{{ $backup->uuid }}-{{ $section }}" />
@endif
</div>
</div>
</div>
@@ -2,19 +2,6 @@
<x-forms.input placeholder="0 0 * * * or daily" id="frequency"
helper="You can use every_minute, hourly, daily, weekly, monthly, yearly or a cron expression." label="Frequency"
required />
<h2>S3</h2>
@if ($definedS3s->count() === 0)
<div class="text-red-500">No validated S3 Storages found.</div>
@else
<x-forms.checkbox wire:model.live="saveToS3" label="Save to S3" />
@if ($saveToS3)
<x-forms.select id="s3StorageId" label="Select a S3 Storage">
@foreach ($definedS3s as $s3)
<option value="{{ $s3->id }}">{{ $s3->name }}</option>
@endforeach
</x-forms.select>
@endif
@endif
<x-forms.button type="submit" @click="modalOpen=false">
Save
</x-forms.button>
@@ -2,14 +2,14 @@
@php
$databasePageItems = [
['label' => 'Configuration', 'route' => 'project.database.configuration', 'active' => request()->routeIs('project.database.configuration')],
['label' => 'Logs', 'route' => 'project.database.logs', 'active' => request()->routeIs('project.database.logs')],
['label' => 'Terminal', 'route' => 'project.database.command', 'active' => request()->routeIs('project.database.command'), 'navigate' => false, 'visible' => auth()->user()?->can('canAccessTerminal')],
[
'label' => 'Backups',
'route' => 'project.database.backup.index',
'active' => request()->routeIs('project.database.backup.index', 'project.database.backup.execution'),
'active' => request()->routeIs('project.database.backup.*'),
'visible' => $database->isBackupSolutionAvailable(),
],
['label' => 'Logs', 'route' => 'project.database.logs', 'active' => request()->routeIs('project.database.logs')],
['label' => 'Terminal', 'route' => 'project.database.command', 'active' => request()->routeIs('project.database.command'), 'navigate' => false, 'visible' => auth()->user()?->can('canAccessTerminal')],
];
$databaseConfigurationItems = [
@@ -185,6 +185,12 @@
Configuration
</a>
@if ($database->isBackupSolutionAvailable())
<a class="shrink-0 {{ request()->routeIs('project.database.backup.*') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('project.database.backup.index', $parameters) }}">
Backups
</a>
@endif
<a class="shrink-0 {{ request()->routeIs('project.database.logs') ? 'dark:text-white' : '' }}"
href="{{ route('project.database.logs', $parameters) }}">
Logs
@@ -195,12 +201,7 @@
Terminal
</a>
@endcan
@if ($database->isBackupSolutionAvailable())
<a class="shrink-0 {{ request()->routeIs('project.database.backup.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('project.database.backup.index', $parameters) }}">
Backups
</a>
@endif
</nav>
@if ($database->destination->server->isFunctional())
@@ -1,4 +1,16 @@
<div>
<div x-data="{
search: '',
backups: @js($database->scheduledBackups->map(fn ($backup) => [
'name' => strtolower($database->name),
'frequency' => strtolower($backup->frequency),
's3_storage' => strtolower($backup->s3?->name ?? ''),
])->values()),
hasMatches() {
const query = this.search.toLowerCase();
return this.backups.some((backup) => backup.name.includes(query) || backup.frequency.includes(query) || backup.s3_storage.includes(query));
},
}">
<div class="flex flex-col gap-2">
@if ($database->is_migrated && blank($database->custom_type))
<div>
@@ -18,9 +30,16 @@
</form>
</div>
@else
<div class="max-w-md pb-4">
<x-forms.input id="null" type="search" x-model="search"
placeholder="Search by database name, frequency, or S3 storage..." />
</div>
<div x-cloak x-show="search !== '' && backups.length > 0 && !hasMatches()">
No scheduled backups match your search.
</div>
@forelse($database->scheduledBackups as $backup)
@if ($type == 'database')
<a @class([
<a x-show="search === '' || @js(strtolower($database->name)).includes(search.toLowerCase()) || @js(strtolower($backup->frequency)).includes(search.toLowerCase()) || @js(strtolower($backup->s3?->name ?? '')).includes(search.toLowerCase())" @class([
'flex flex-col border-l-2 transition-colors p-4 cursor-pointer bg-white hover:bg-gray-100 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 text-black dark:text-white',
'border-blue-500/50 border-dashed' =>
$backup->latest_log &&
@@ -94,21 +113,19 @@
@endif
@endif
@if ($backup->save_s3)
• S3: Enabled
• S3: {{ $backup->s3?->name ?? 'Storage unavailable' }}
@endif
@else
Last Run: Never • Total Executions: 0
@if ($backup->save_s3)
• S3: Enabled
• S3: {{ $backup->s3?->name ?? 'Storage unavailable' }}
@endif
@endif
</div>
</a>
@else
<div @class([
<a x-show="search === '' || @js(strtolower($database->name)).includes(search.toLowerCase()) || @js(strtolower($backup->frequency)).includes(search.toLowerCase()) || @js(strtolower($backup->s3?->name ?? '')).includes(search.toLowerCase())" @class([
'flex flex-col border-l-2 transition-colors p-4 cursor-pointer bg-white hover:bg-gray-100 dark:bg-coolgray-100 dark:hover:bg-coolgray-200 text-black dark:text-white',
'bg-gray-200 dark:bg-coolgray-200' =>
data_get($backup, 'id') === data_get($selectedBackup, 'id'),
'border-blue-500/50 border-dashed' =>
$backup->latest_log &&
data_get($backup->latest_log, 'status') === 'running',
@@ -119,9 +136,8 @@
$backup->latest_log &&
data_get($backup->latest_log, 'status') === 'success',
'border-gray-200 dark:border-coolgray-300' => !$backup->latest_log,
'border-coollabs' =>
data_get($backup, 'id') === data_get($selectedBackup, 'id'),
]) wire:click="setSelectedBackup('{{ data_get($backup, 'id') }}')">
]) {{ wireNavigate() }}
href="{{ route('project.service.database.backup.show', [...$parameters, 'backup_uuid' => $backup->uuid]) }}">
@if ($backup->latest_log && data_get($backup->latest_log, 'status') === 'running')
<div class="absolute top-2 right-2">
<x-loading />
@@ -182,7 +198,7 @@
@endif
@endif
@if ($backup->save_s3)
• S3: Enabled
• S3: {{ $backup->s3?->name ?? 'Storage unavailable' }}
@endif
<br>Total Executions: {{ $backup->executions()->count() }}
@php
@@ -202,23 +218,15 @@
@else
Last Run: Never • Total Executions: 0
@if ($backup->save_s3)
• S3: Enabled
• S3: {{ $backup->s3?->name ?? 'Storage unavailable' }}
@endif
@endif
</div>
</div>
</a>
@endif
@empty
<div>No scheduled backups configured.</div>
@endforelse
@endif
</div>
@if ($type === 'service-database' && $selectedBackup)
<div class="pt-10">
<livewire:project.database.backup-edit wire:key="{{ $selectedBackup->id }}" :backup="$selectedBackup"
:available-s3-storages="$s3s" :status="data_get($database, 'status')" />
<livewire:project.database.backup-executions wire:key="{{ $selectedBackup->uuid }}" :backup="$selectedBackup"
:database="$database" />
</div>
@endif
</div>
@@ -1,23 +1,65 @@
<div>
<livewire:project.service.heading :service="$service" :parameters="$parameters" :query="$query" />
<div class="flex flex-col h-full gap-2 md:gap-8 md:flex-row">
<x-service-database.sidebar :parameters="$parameters" :serviceDatabase="$serviceDatabase" :isImportSupported="$isImportSupported" />
@if ($backup)
<div class="sub-menu-wrapper">
<a class="sub-menu-item" {{ wireNavigate() }}
href="{{ route('project.service.database.backups', $parameters) }}">
<span class="menu-item-label">Back</span>
</a>
<a @class(['sub-menu-item', 'menu-item-active' => $section === 'general']) {{ wireNavigate() }}
href="{{ route('project.service.database.backup.show', $backupParameters) }}">
<span class="menu-item-label">General</span>
</a>
<a @class(['sub-menu-item', 'menu-item-active' => $section === 's3']) {{ wireNavigate() }}
href="{{ route('project.service.database.backup.s3', $backupParameters) }}">
<span class="menu-item-label">S3</span>
</a>
<a @class(['sub-menu-item', 'menu-item-active' => $section === 'retention']) {{ wireNavigate() }}
href="{{ route('project.service.database.backup.retention', $backupParameters) }}">
<span class="menu-item-label">Retention</span>
</a>
<a @class(['sub-menu-item', 'menu-item-active' => $section === 'executions']) {{ wireNavigate() }}
href="{{ route('project.service.database.backup.executions', $backupParameters) }}">
<span class="menu-item-label">Executions</span>
</a>
<a @class(['sub-menu-item', 'menu-item-active' => $section === 'danger']) {{ wireNavigate() }}
href="{{ route('project.service.database.backup.danger', $backupParameters) }}">
<span class="menu-item-label">Danger Zone</span>
</a>
</div>
@else
<x-service-database.sidebar :parameters="$parameters" :serviceDatabase="$serviceDatabase"
:isImportSupported="$isImportSupported" />
@endif
<div class="w-full">
<x-slot:title>
{{ data_get_str($service, 'name')->limit(10) }} >
{{ data_get_str($serviceDatabase, 'name')->limit(10) }} > Backups | Coolify
</x-slot>
<div class="flex gap-2">
<h2 class="pb-4">Scheduled Backups</h2>
@if (filled($serviceDatabase->custom_type) || !$serviceDatabase->is_migrated)
@can('update', $serviceDatabase)
<x-modal-input buttonTitle="+ Add" title="New Scheduled Backup">
<livewire:project.database.create-scheduled-backup :database="$serviceDatabase" />
</x-modal-input>
@endcan
@if ($backup)
@if ($section === 'executions')
<livewire:project.database.backup-executions :backup="$backup" :database="$serviceDatabase" />
@else
<livewire:project.database.backup-edit :backup="$backup" :available-s3-storages="$s3s"
:status="data_get($serviceDatabase, 'status')" :section="$section"
wire:key="service-database-backup-{{ $backup->uuid }}-{{ $section }}" />
@endif
</div>
<livewire:project.database.scheduled-backups :database="$serviceDatabase" />
@else
<div class="flex gap-2">
<h2 class="pb-4">Scheduled Backups</h2>
@if (filled($serviceDatabase->custom_type) || !$serviceDatabase->is_migrated)
@can('update', $serviceDatabase)
<x-modal-input buttonTitle="+ Add" title="New Scheduled Backup">
<livewire:project.database.create-scheduled-backup :database="$serviceDatabase" />
</x-modal-input>
@endcan
@endif
</div>
<livewire:project.database.scheduled-backups :database="$serviceDatabase" />
@endif
</div>
</div>
</div>
@@ -19,7 +19,15 @@
@endif
<div class="flex flex-col justify-center text-sm select-text">
<div class="flex gap-2 md:flex-row flex-col">
<x-forms.input label="Source Path" :value="$fileStorage->fs_path" readonly />
<x-forms.input label="Source Path" :value="$fileStorage->fs_path" readonly>
<x-slot:labelSuffix>
@if ($hasEnabledBackup)
<x-status-badge :as="$backupUrl ? 'a' : 'span'" :href="$backupUrl"
status="Backup enabled" type="success"
:class="$backupUrl ? 'cursor-pointer underline' : null" />
@endif
</x-slot:labelSuffix>
</x-forms.input>
<x-forms.input label="Destination Path" :value="$fileStorage->mount_path" readonly />
</div>
</div>
@@ -51,6 +59,14 @@
confirmationText="{{ $fs_path }}"
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
shortConfirmationLabel="Filepath" :confirmWithPassword="false" step2ButtonText="Convert to file" />
@if ($resource instanceof \App\Models\Application)
<x-modal-input buttonTitle="Configure Backup" title="Configure Directory Backup"
:wireIgnore="false">
<livewire:project.application.backup.create :application="$resource"
:selected-target-key="'directory:' . $fileStorage->id"
wire:key="configure-directory-backup-{{ $fileStorage->id }}" />
</x-modal-input>
@endif
<x-modal-confirmation :ignoreWire="false" title="Confirm Directory Deletion?" buttonTitle="Delete"
isErrorButton submitAction="delete" :checkboxes="$directoryDeletionCheckboxes" :actions="[
'The selected directory and all its contents will be permanently deleted from the container.',
@@ -130,5 +146,16 @@
@endif
@endif
</form>
@if ($isReadOnly && $fileStorage->is_directory && $resource instanceof \App\Models\Application)
@can('update', $resource)
<div>
<x-modal-input buttonTitle="Configure Backup" title="Configure Directory Backup" :wireIgnore="false">
<livewire:project.application.backup.create :application="$resource"
:selected-target-key="'directory:' . $fileStorage->id"
wire:key="configure-readonly-directory-backup-{{ $fileStorage->id }}" />
</x-modal-input>
</div>
@endcan
@endif
</div>
</div>
@@ -23,7 +23,7 @@
['label' => 'Back', 'route' => 'project.service.configuration', 'active' => false, 'parameters' => [...$parameters, 'stack_service_uuid' => null]],
['label' => 'General', 'route' => 'project.service.index', 'active' => request()->routeIs('project.service.index')],
['label' => 'Advanced', 'route' => 'project.service.index.advanced', 'active' => request()->routeIs('project.service.index.advanced')],
['label' => 'Backups', 'route' => 'project.service.database.backups', 'active' => request()->routeIs('project.service.database.backups')],
['label' => 'Backups', 'route' => 'project.service.database.backups', 'active' => request()->routeIs('project.service.database.backup*')],
['label' => 'Import Backup', 'route' => 'project.service.database.import', 'active' => request()->routeIs('project.service.database.import')],
];
}
@@ -55,6 +55,7 @@
<h3 class="text-2xl font-bold text-black dark:text-white">Configuration changes</h3>
<p class="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
These changes are not applied to the latest deployment yet.
(If you think it is incorrect, just ignore)
</p>
</div>
<button type="button" @click="configurationDiffModalOpen = false"
@@ -12,10 +12,26 @@
$storage->resource_type === 'App\Models\ServiceApplication' ||
$storage->resource_type === 'App\Models\ServiceDatabase')
<x-forms.input id="name" label="Volume Name" required readonly
helper="Warning: Changing the volume name after the initial start could cause problems. Only use it when you know what are you doing." />
helper="Warning: Changing the volume name after the initial start could cause problems. Only use it when you know what are you doing.">
<x-slot:labelSuffix>
@if ($hasEnabledBackup)
<x-status-badge :as="$backupUrl ? 'a' : 'span'" :href="$backupUrl"
status="Backup enabled" type="success"
:class="$backupUrl ? 'cursor-pointer underline' : null" />
@endif
</x-slot:labelSuffix>
</x-forms.input>
@else
<x-forms.input id="name" label="Volume Name" required readonly
helper="Warning: Changing the volume name after the initial start could cause problems. Only use it when you know what are you doing." />
helper="Warning: Changing the volume name after the initial start could cause problems. Only use it when you know what are you doing.">
<x-slot:labelSuffix>
@if ($hasEnabledBackup)
<x-status-badge :as="$backupUrl ? 'a' : 'span'" :href="$backupUrl"
status="Backup enabled" type="success"
:class="$backupUrl ? 'cursor-pointer underline' : null" />
@endif
</x-slot:labelSuffix>
</x-forms.input>
@endif
@if ($isService || $startedAt)
<x-forms.input id="hostPath" readonly helper="Directory on the host system."
@@ -33,7 +49,15 @@
</div>
@else
<div class="flex gap-2 items-end w-full">
<x-forms.input id="name" required readonly />
<x-forms.input id="name" :label="$hasEnabledBackup ? 'Volume Name' : null" required readonly>
<x-slot:labelSuffix>
@if ($hasEnabledBackup)
<x-status-badge :as="$backupUrl ? 'a' : 'span'" :href="$backupUrl"
status="Backup enabled" type="success"
:class="$backupUrl ? 'cursor-pointer underline' : null" />
@endif
</x-slot:labelSuffix>
</x-forms.input>
<x-forms.input id="hostPath" readonly />
<x-forms.input id="mountPath" required readonly />
</div>
@@ -47,18 +71,43 @@
</div>
@endcan
@endif
@if ($resource instanceof \App\Models\Application)
@can('update', $resource)
<x-modal-input buttonTitle="Configure Backup" title="Configure Volume Backup" :wireIgnore="false">
<livewire:project.application.backup.create :application="$resource"
:selected-target-key="'volume:' . $storage->id"
wire:key="configure-readonly-volume-backup-{{ $storage->id }}" />
</x-modal-input>
@endcan
@endif
@else
@can('update', $resource)
@if ($isFirst)
<div class="flex gap-2 items-end w-full">
<x-forms.input id="name" label="Volume Name" required />
<x-forms.input id="name" label="Volume Name" required>
<x-slot:labelSuffix>
@if ($hasEnabledBackup)
<x-status-badge :as="$backupUrl ? 'a' : 'span'" :href="$backupUrl"
status="Backup enabled" type="success"
:class="$backupUrl ? 'cursor-pointer underline' : null" />
@endif
</x-slot:labelSuffix>
</x-forms.input>
<x-forms.input id="hostPath" helper="Directory on the host system." label="Source Path" />
<x-forms.input id="mountPath" label="Destination Path"
helper="Directory inside the container." required />
</div>
@else
<div class="flex gap-2 items-end w-full">
<x-forms.input id="name" required />
<x-forms.input id="name" :label="$hasEnabledBackup ? 'Volume Name' : null" required>
<x-slot:labelSuffix>
@if ($hasEnabledBackup)
<x-status-badge :as="$backupUrl ? 'a' : 'span'" :href="$backupUrl"
status="Backup enabled" type="success"
:class="$backupUrl ? 'cursor-pointer underline' : null" />
@endif
</x-slot:labelSuffix>
</x-forms.input>
<x-forms.input id="hostPath" />
<x-forms.input id="mountPath" required />
</div>
@@ -74,6 +123,13 @@
<x-forms.button type="submit">
Update
</x-forms.button>
@if ($resource instanceof \App\Models\Application)
<x-modal-input buttonTitle="Configure Backup" title="Configure Volume Backup" :wireIgnore="false">
<livewire:project.application.backup.create :application="$resource"
:selected-target-key="'volume:' . $storage->id"
wire:key="configure-volume-backup-{{ $storage->id }}" />
</x-modal-input>
@endif
<x-modal-confirmation title="Confirm persistent storage deletion?" isErrorButton buttonTitle="Delete"
submitAction="delete" :actions="[
'The selected persistent storage/volume will be permanently deleted.',
@@ -85,7 +141,15 @@
@else
@if ($isFirst)
<div class="flex gap-2 items-end w-full">
<x-forms.input id="name" label="Volume Name" required disabled />
<x-forms.input id="name" label="Volume Name" required disabled>
<x-slot:labelSuffix>
@if ($hasEnabledBackup)
<x-status-badge :as="$backupUrl ? 'a' : 'span'" :href="$backupUrl"
status="Backup enabled" type="success"
:class="$backupUrl ? 'cursor-pointer underline' : null" />
@endif
</x-slot:labelSuffix>
</x-forms.input>
<x-forms.input id="hostPath" helper="Directory on the host system." label="Source Path"
disabled />
<x-forms.input id="mountPath" label="Destination Path"
@@ -93,7 +157,15 @@
</div>
@else
<div class="flex gap-2 items-end w-full">
<x-forms.input id="name" required disabled />
<x-forms.input id="name" :label="$hasEnabledBackup ? 'Volume Name' : null" required disabled>
<x-slot:labelSuffix>
@if ($hasEnabledBackup)
<x-status-badge :as="$backupUrl ? 'a' : 'span'" :href="$backupUrl"
status="Backup enabled" type="success"
:class="$backupUrl ? 'cursor-pointer underline' : null" />
@endif
</x-slot:labelSuffix>
</x-forms.input>
<x-forms.input id="hostPath" disabled />
<x-forms.input id="mountPath" required disabled />
</div>
@@ -0,0 +1,21 @@
<div>
@if ($section === 'retention')
@include('livewire.project.shared.storages.volume-backups.retention')
@elseif ($section === 's3')
@include('livewire.project.shared.storages.volume-backups.s3')
@elseif ($section === 'executions')
@include('livewire.project.shared.storages.volume-backups.executions')
@elseif ($section === 'danger')
@include('livewire.project.shared.storages.volume-backups.danger')
@else
@include('livewire.project.shared.storages.volume-backups.general')
@endif
@script
<script>
window.download_volume_backup_file = function(executionId) {
window.open('/download/volume-backup/' + executionId, '_blank');
}
</script>
@endscript
</div>
@@ -0,0 +1,24 @@
<div class="flex flex-col gap-6">
<div>
<h2>Danger Zone</h2>
<div class="text-neutral-500">Woah. I hope you know what you are doing.</div>
</div>
<div>
<h3>Delete Scheduled Backup</h3>
<div class="pb-4 text-neutral-500">
This permanently deletes the schedule and all local and S3 archives created by it. There is no coming back.
</div>
@if ($backup)
<x-modal-confirmation title="Confirm Backup Schedule Deletion?" isErrorButton submitAction="delete"
:actions="['The selected backup schedule will be deleted.', 'All local and S3 archives created by this schedule will be deleted.']"
confirmationText="{{ $backup->targetName() }}"
confirmationLabel="Please confirm by entering the {{ $backup->targetType() }} identifier below"
shortConfirmationLabel="{{ $backup->targetType() }} identifier">
<x-slot:trigger>
<x-forms.button isError>Delete Backups and Schedule</x-forms.button>
</x-slot:trigger>
</x-modal-confirmation>
@endif
</div>
</div>
@@ -0,0 +1,183 @@
@if ($backup)
@php
$executionCount = method_exists($executions, 'total') ? $executions->total() : $executions->count();
$currentPage = method_exists($executions, 'currentPage') ? $executions->currentPage() : 1;
$lastPage = method_exists($executions, 'lastPage') ? $executions->lastPage() : 1;
@endphp
<div wire:poll.5000ms="$refresh">
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
<h2>Executions</h2>
@if ($executionCount > 0)
<div class="flex items-center gap-2">
<x-forms.button :disabled="$currentPage === 1" wire:click="previousPage">
<svg class="w-4 h-4" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2" d="m14 6l-6 6l6 6z" />
</svg>
</x-forms.button>
<span class="px-2 text-sm text-gray-600 dark:text-gray-400">
Page {{ $currentPage }} of {{ $lastPage }}
</span>
<x-forms.button :disabled="$currentPage === $lastPage" wire:click="nextPage">
<svg class="w-4 h-4" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2" d="m10 18l6-6l-6-6z" />
</svg>
</x-forms.button>
</div>
@endif
<div class="flex flex-col gap-2 sm:flex-row sm:flex-wrap">
<x-forms.button wire:click="cleanupFailed" class="w-full sm:w-auto">
Cleanup Failed Backups
</x-forms.button>
<x-modal-confirmation title="Cleanup Deleted Backup Entries?" isErrorButton
submitAction="cleanupDeleted()"
:actions="['This permanently deletes backup execution entries whose local and S3 files have already been deleted.', 'This only removes database entries, not backup files.']"
confirmationText="cleanup deleted backups"
confirmationLabel="Please confirm by typing 'cleanup deleted backups' below"
shortConfirmationLabel="Confirmation">
<x-slot:trigger>
<x-forms.button isError class="w-full sm:w-auto">Cleanup Deleted</x-forms.button>
</x-slot:trigger>
</x-modal-confirmation>
</div>
</div>
<div class="flex flex-col gap-4 pt-2">
@forelse ($executions as $execution)
<div wire:key="volume-backup-execution-{{ $execution->id }}" @class([
'relative flex flex-col border-l-2 transition-colors p-4 bg-white dark:bg-coolgray-100 text-black dark:text-white',
'border-blue-500/50 border-dashed' => $execution->status === 'running',
'border-error' => $execution->status === 'failed',
'border-success' => $execution->status === 'success',
])>
@if ($execution->status === 'running')
<div class="absolute top-2 right-2"><x-loading /></div>
@endif
<div class="flex items-center gap-2 mb-2">
<span @class([
'px-3 py-1 rounded-md text-xs font-medium tracking-wide shadow-xs',
'bg-blue-100/80 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300' => $execution->status === 'running',
'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200' => $execution->status === 'failed',
'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-200' => $execution->status === 'success' && $execution->s3_uploaded === false,
'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200' => $execution->status === 'success' && $execution->s3_uploaded !== false,
])>
@if ($execution->status === 'running')
In Progress
@elseif ($execution->status === 'success' && $execution->s3_uploaded === false)
Success (S3 Warning)
@else
{{ ucfirst($execution->status) }}
@endif
</span>
</div>
<div class="text-sm text-gray-600 dark:text-gray-400">
@if ($execution->status === 'running')
<span title="Started: {{ $execution->created_at->toDateTimeString() }}">
Running for {{ calculateDuration($execution->created_at, now()) }}
</span>
@else
@php
$finishedAt = $execution->finished_at ?? $execution->updated_at;
@endphp
<span title="Started: {{ $execution->created_at->toDateTimeString() }}&#10;Ended: {{ $finishedAt->toDateTimeString() }}">
{{ $finishedAt->diffForHumans() }}
({{ calculateDuration($execution->created_at, $finishedAt) }})
• {{ $finishedAt->format('M j, H:i') }}
</span>
@endif
• {{ $backup->targetType() }}: {{ $backup->targetName() }}
@if ($execution->size > 0)
• Size: {{ formatBytes($execution->size) }}
@endif
</div>
<div class="text-sm text-gray-600 dark:text-gray-400">
Location: {{ $execution->filename ?? 'N/A' }}
</div>
<div class="flex flex-col gap-2 mt-2 sm:flex-row sm:flex-wrap sm:items-center sm:gap-3">
<div class="text-sm text-gray-600 dark:text-gray-400">Backup Availability:</div>
<span @class([
'px-2 py-1 rounded-sm text-xs font-medium',
'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200' => !$execution->local_storage_deleted,
'bg-gray-100 text-gray-600 dark:bg-gray-800/50 dark:text-gray-400' => $execution->local_storage_deleted,
])>
<span class="flex items-center gap-1">
@if (!$execution->local_storage_deleted)
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
@else
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
@endif
Local Storage
</span>
</span>
@if ($execution->s3_uploaded !== null)
<span @class([
'px-2 py-1 rounded-sm text-xs font-medium',
'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200' => $execution->s3_uploaded === false && !$execution->s3_storage_deleted,
'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200' => $execution->s3_uploaded === true && !$execution->s3_storage_deleted,
'bg-gray-100 text-gray-600 dark:bg-gray-800/50 dark:text-gray-400' => $execution->s3_storage_deleted,
])>
<span class="flex items-center gap-1">
@if ($execution->s3_uploaded === true && !$execution->s3_storage_deleted)
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
@else
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
@endif
S3 Storage
</span>
</span>
@endif
</div>
@if ($execution->message)
<div class="p-2 mt-2 bg-gray-100 rounded-sm dark:bg-coolgray-200">
<pre class="text-sm whitespace-pre-wrap">{{ $execution->message }}</pre>
</div>
@endif
@if ($execution->status !== 'running')
<div class="grid grid-cols-2 gap-2 mt-4 sm:flex sm:flex-wrap">
@if ($execution->status === 'success' && !$execution->local_storage_deleted)
<x-forms.button class="w-full dark:hover:bg-coolgray-400 sm:w-auto"
x-on:click="download_volume_backup_file('{{ $execution->id }}')">
Download
</x-forms.button>
@endif
@php
$executionCheckboxes = [];
$deleteActions = [];
if (!$execution->local_storage_deleted) {
$deleteActions[] = 'This backup will be permanently deleted from local storage.';
}
if ($execution->s3_uploaded === true && !$execution->s3_storage_deleted) {
$executionCheckboxes[] = ['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently from S3 Storage'];
}
if (empty($deleteActions)) {
$deleteActions[] = 'This backup execution record will be deleted.';
}
@endphp
<x-modal-confirmation title="Confirm Backup Deletion?" isErrorButton
submitAction="deleteBackup({{ $execution->id }})"
:checkboxes="$executionCheckboxes" :actions="$deleteActions"
confirmationText="{{ $execution->filename }}"
confirmationLabel="Please confirm the execution of the actions by entering the Backup Filename below"
shortConfirmationLabel="Backup Filename">
<x-slot:trigger>
<x-forms.button isError class="w-full sm:w-auto">Delete</x-forms.button>
</x-slot:trigger>
</x-modal-confirmation>
</div>
@endif
</div>
@empty
<div class="p-4 bg-gray-100 rounded-sm dark:bg-coolgray-100">No executions found.</div>
@endforelse
</div>
</div>
@endif
@@ -0,0 +1,47 @@
<form wire:submit="save" class="flex flex-col gap-4">
<div>
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
<h2>General</h2>
<div class="flex flex-col gap-2 sm:flex-row sm:flex-wrap">
<x-forms.button type="submit" class="w-full sm:w-auto">Save</x-forms.button>
@if (!$enabled)
<x-forms.button type="button" wire:click="toggleEnabled" wire:loading.attr="disabled"
wire:target="toggleEnabled" isHighlighted>Enable Backup</x-forms.button>
@else
<x-forms.button type="button" wire:click="toggleEnabled" wire:loading.attr="disabled"
wire:target="toggleEnabled">Disable Backup</x-forms.button>
@endif
<x-forms.button type="button" wire:click="backupNow" class="w-full sm:w-auto">Backup Now</x-forms.button>
</div>
</div>
<p class="pt-1 text-sm text-neutral-600 dark:text-neutral-400">
{{ $backup?->targetType() ?? ($storage instanceof \App\Models\LocalFileVolume ? 'Directory' : 'Volume') }}:
<span class="font-medium text-neutral-800 dark:text-neutral-200">
{{ $backup?->targetName() ?? ($storage instanceof \App\Models\LocalFileVolume ? $storage->fs_path : $storage->name) }}
</span>
</p>
</div>
<div class="p-3 text-sm rounded bg-warning/10 text-warning">
Backups made while the application is writing to this storage may be inconsistent or corrupted. You can
gracefully stop containers during the archive step for a safer file-level backup, but this briefly
interrupts the application.
</div>
<div class="w-full max-w-md">
<x-forms.checkbox instantSave id="stopDuringBackup" label="Stop containers while creating the archive"
helper="Off by default. Containers using this storage are gracefully stopped and restarted immediately after the archive is created." />
</div>
<div class="flex flex-col gap-4">
<div class="grid grid-cols-1 gap-2 md:grid-cols-3">
<x-forms.input id="frequency" label="Frequency" required
helper="Use every_minute, hourly, daily, weekly, monthly, yearly, or a cron expression." />
<x-forms.input id="timezone" label="Timezone" disabled
helper="The timezone of the server where the backup is scheduled to run (if not set, the instance timezone will be used)" required />
<x-forms.input id="timeout" type="number" min="60" max="36000" label="Timeout"
helper="The timeout of the backup job in seconds." required />
</div>
</div>
</form>
@@ -0,0 +1,52 @@
<form wire:submit="save" class="flex flex-col gap-4">
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
<h2>Retention</h2>
<x-forms.button type="submit" class="w-full sm:w-auto" canGate="update"
:canResource="$backup">Save</x-forms.button>
</div>
<div>
<ul class="pl-6 space-y-2 list-disc">
<li>Setting a value to 0 means unlimited retention.</li>
<li>The retention rules work independently - whichever limit is reached first will trigger cleanup.</li>
</ul>
</div>
<div class="flex flex-col gap-6">
<div>
<h3 class="mb-3">Local Backup Retention</h3>
<div class="grid grid-cols-1 gap-2 md:grid-cols-3">
<x-forms.input label="Number of backups to keep" id="retentionAmountLocally" type="number"
min="0" canGate="update" :canResource="$backup"
helper="Keeps only the specified number of most recent backups on the server. Set to 0 for unlimited backups."
required />
<x-forms.input label="Days to keep backups" id="retentionDaysLocally" type="number"
min="0" canGate="update" :canResource="$backup"
helper="Automatically removes backups older than the specified number of days. Set to 0 for no time limit."
required />
<x-forms.input label="Maximum storage (GB)" id="retentionMaxStorageLocally" type="number"
min="0" step="any" canGate="update" :canResource="$backup"
helper="When total size of all backups in the current backup job exceeds this limit in GB, the oldest backups will be removed. Decimal values are supported (e.g. 0.001 for 1MB). Set to 0 for unlimited storage."
required />
</div>
</div>
<div>
<h3 class="mb-3">S3 Storage Retention</h3>
<div class="grid grid-cols-1 gap-2 md:grid-cols-3">
<x-forms.input label="Number of backups to keep" id="retentionAmountS3" type="number" min="0"
canGate="update" :canResource="$backup"
helper="Keeps only the specified number of most recent backups on S3 storage. Set to 0 for unlimited backups."
required />
<x-forms.input label="Days to keep backups" id="retentionDaysS3" type="number" min="0"
canGate="update" :canResource="$backup"
helper="Automatically removes S3 backups older than the specified number of days. Set to 0 for no time limit."
required />
<x-forms.input label="Maximum storage (GB)" id="retentionMaxStorageS3" type="number" min="0"
step="any" canGate="update" :canResource="$backup"
helper="When total size of all backups in the current backup job exceeds this limit in GB, the oldest backups will be removed. Decimal values are supported (e.g. 0.5 for 500MB). Set to 0 for unlimited storage."
required />
</div>
</div>
</div>
</form>
@@ -0,0 +1,54 @@
@if ($availableS3Storages->isEmpty())
<div class="flex flex-col gap-4">
<h2>S3</h2>
<div>
No validated S3 available. Configure one <a class="underline dark:text-white" {{ wireNavigate() }}
href="{{ route('storage.index') }}">here</a>.
</div>
</div>
@else
<form wire:submit="save" class="flex flex-col gap-4">
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
<h2>S3</h2>
<x-forms.button type="submit" class="w-full sm:w-auto" canGate="update"
:canResource="$backup">Save</x-forms.button>
@if (!$saveToS3)
<x-forms.button type="button" wire:click="toggleS3" wire:loading.attr="disabled"
wire:target="toggleS3" isHighlighted canGate="update" :canResource="$backup">Enable S3</x-forms.button>
@else
<x-forms.button type="button" wire:click="toggleS3" wire:loading.attr="disabled"
wire:target="toggleS3" canGate="update" :canResource="$backup">Disable S3</x-forms.button>
@endif
</div>
<div class="w-full max-w-md pb-2">
<div class="flex items-center gap-1 mb-1 text-sm font-medium">
<span>S3 Storage</span>
@if (!$saveToS3)
<span class="text-xs font-normal text-warning">(currently disabled)</span>
@else
<x-highlighted text="*" />
@endif
</div>
<x-forms.select id="s3StorageId" wire:model.live="s3StorageId" :required="$saveToS3" canGate="update"
:canResource="$backup">
@foreach ($availableS3Storages as $s3Storage)
<option value="{{ $s3Storage->id }}">{{ $s3Storage->name }}</option>
@endforeach
</x-forms.select>
</div>
<div class="w-full max-w-md">
@if ($saveToS3)
<x-forms.checkbox instantSave id="disableLocalBackup" label="Disable Local Backup"
helper="When enabled, backup files are deleted locally after a successful S3 upload." canGate="update"
:canResource="$backup" />
@else
<x-forms.checkbox id="disableLocalBackup" label="Disable Local Backup"
helper="When enabled, backup files are deleted locally after a successful S3 upload." disabled
canGate="update" :canResource="$backup" />
@endif
</div>
</form>
@endif
@@ -20,7 +20,7 @@
submitAction="delete({{ $storage->id }})" :actions="array_filter([
'The selected storage location will be permanently deleted from Coolify.',
$backupCount > 0
? $backupCount . ' backup schedule(s) will be updated to no longer save to S3 and will only store backups locally on the server.'
? $backupCount . ' backup schedule(s) will stop saving to S3. Existing objects in this storage will not be deleted.'
: null,
])" confirmationText="{{ $storage->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Storage Name below"