feat(deployment): add pull request filtering and pagination to deployment and backup execution components
fix(ui): make them more stylish yeah
This commit is contained in:
		
							
								
								
									
										87
									
								
								CLAUDE.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								CLAUDE.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | |||||||
|  | # CLAUDE.md | ||||||
|  | 
 | ||||||
|  | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | ||||||
|  | 
 | ||||||
|  | ## Project Overview | ||||||
|  | 
 | ||||||
|  | Coolify is an open-source, self-hostable platform for deploying applications and managing servers - an alternative to Heroku/Netlify/Vercel. It's built with Laravel (PHP) and uses Docker for containerization. | ||||||
|  | 
 | ||||||
|  | ## Development Commands | ||||||
|  | 
 | ||||||
|  | ### Frontend Development | ||||||
|  | - `npm run dev` - Start Vite development server for frontend assets | ||||||
|  | - `npm run build` - Build frontend assets for production | ||||||
|  | 
 | ||||||
|  | ### Backend Development | ||||||
|  | - `php artisan serve` - Start Laravel development server | ||||||
|  | - `php artisan migrate` - Run database migrations | ||||||
|  | - `php artisan queue:work` - Start queue worker for background jobs | ||||||
|  | - `php artisan horizon` - Start Laravel Horizon for queue monitoring | ||||||
|  | - `php artisan tinker` - Start interactive PHP REPL | ||||||
|  | 
 | ||||||
|  | ### Code Quality | ||||||
|  | - `./vendor/bin/pint` - Run Laravel Pint for code formatting | ||||||
|  | - `./vendor/bin/phpstan` - Run PHPStan for static analysis | ||||||
|  | - `./vendor/bin/pest` - Run Pest tests | ||||||
|  | 
 | ||||||
|  | ## Architecture Overview | ||||||
|  | 
 | ||||||
|  | ### Technology Stack | ||||||
|  | - **Backend**: Laravel 12 (PHP 8.4) | ||||||
|  | - **Frontend**: Livewire + Alpine.js + Tailwind CSS | ||||||
|  | - **Database**: PostgreSQL 15 | ||||||
|  | - **Cache/Queue**: Redis 7 | ||||||
|  | - **Real-time**: Soketi (WebSocket server) | ||||||
|  | - **Containerization**: Docker & Docker Compose | ||||||
|  | 
 | ||||||
|  | ### Key Components | ||||||
|  | 
 | ||||||
|  | #### Core Models | ||||||
|  | - `Application` - Deployed applications with Git integration | ||||||
|  | - `Server` - Remote servers managed by Coolify | ||||||
|  | - `Service` - Docker Compose services | ||||||
|  | - `Database` - Standalone database instances (PostgreSQL, MySQL, MongoDB, Redis, etc.) | ||||||
|  | - `Team` - Multi-tenancy support | ||||||
|  | - `Project` - Grouping of environments and resources | ||||||
|  | 
 | ||||||
|  | #### Job System | ||||||
|  | - Uses Laravel Horizon for queue management | ||||||
|  | - Key jobs: `ApplicationDeploymentJob`, `ServerCheckJob`, `DatabaseBackupJob` | ||||||
|  | - `ScheduledJobManager` and `ServerResourceManager` handle job scheduling | ||||||
|  | 
 | ||||||
|  | #### Deployment Flow | ||||||
|  | 1. Git webhook triggers deployment | ||||||
|  | 2. `ApplicationDeploymentJob` handles build and deployment | ||||||
|  | 3. Docker containers are managed on target servers | ||||||
|  | 4. Proxy configuration (Nginx/Traefik) is updated | ||||||
|  | 
 | ||||||
|  | #### Server Management | ||||||
|  | - SSH-based server communication via `ExecuteRemoteCommand` trait | ||||||
|  | - Docker installation and management | ||||||
|  | - Proxy configuration generation | ||||||
|  | - Resource monitoring and cleanup | ||||||
|  | 
 | ||||||
|  | ### Directory Structure | ||||||
|  | - `app/Actions/` - Domain-specific actions (Application, Database, Server, etc.) | ||||||
|  | - `app/Jobs/` - Background queue jobs | ||||||
|  | - `app/Livewire/` - Frontend components (full-stack with Livewire) | ||||||
|  | - `app/Models/` - Eloquent models | ||||||
|  | - `bootstrap/helpers/` - Helper functions for various domains | ||||||
|  | - `database/migrations/` - Database schema evolution | ||||||
|  | 
 | ||||||
|  | ## Development Guidelines | ||||||
|  | 
 | ||||||
|  | ### Code Organization | ||||||
|  | - Use Actions pattern for complex business logic | ||||||
|  | - Livewire components handle UI and user interactions   | ||||||
|  | - Jobs handle asynchronous operations | ||||||
|  | - Traits provide shared functionality (e.g., `ExecuteRemoteCommand`) | ||||||
|  | 
 | ||||||
|  | ### Testing | ||||||
|  | - Uses Pest for testing framework | ||||||
|  | - Tests located in `tests/` directory | ||||||
|  | 
 | ||||||
|  | ### Deployment and Docker | ||||||
|  | - Applications are deployed using Docker containers | ||||||
|  | - Configuration generated dynamically based on application settings | ||||||
|  | - Supports multiple deployment targets and proxy configurations | ||||||
| @@ -18,11 +18,13 @@ class Index extends Component | |||||||
| 
 | 
 | ||||||
|     public int $skip = 0; |     public int $skip = 0; | ||||||
| 
 | 
 | ||||||
|     public int $default_take = 10; |     public int $defaultTake = 10; | ||||||
| 
 | 
 | ||||||
|     public bool $show_next = false; |     public bool $showNext = false; | ||||||
| 
 | 
 | ||||||
|     public bool $show_prev = false; |     public bool $showPrev = false; | ||||||
|  | 
 | ||||||
|  |     public int $currentPage = 1; | ||||||
| 
 | 
 | ||||||
|     public ?string $pull_request_id = null; |     public ?string $pull_request_id = null; | ||||||
| 
 | 
 | ||||||
| @@ -51,68 +53,111 @@ class Index extends Component | |||||||
|         if (! $application) { |         if (! $application) { | ||||||
|             return redirect()->route('dashboard'); |             return redirect()->route('dashboard'); | ||||||
|         } |         } | ||||||
|         ['deployments' => $deployments, 'count' => $count] = $application->deployments(0, $this->default_take); |         // Validate pull request ID from URL parameters
 | ||||||
|  |         if ($this->pull_request_id !== null && $this->pull_request_id !== '') { | ||||||
|  |             if (! is_numeric($this->pull_request_id) || (float) $this->pull_request_id <= 0 || (float) $this->pull_request_id != (int) $this->pull_request_id) { | ||||||
|  |                 $this->pull_request_id = null; | ||||||
|  |                 $this->dispatch('error', 'Invalid Pull Request ID in URL. Filter cleared.'); | ||||||
|  |             } else { | ||||||
|  |                 // Ensure it's stored as a string representation of a positive integer
 | ||||||
|  |                 $this->pull_request_id = (string) (int) $this->pull_request_id; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         ['deployments' => $deployments, 'count' => $count] = $application->deployments(0, $this->defaultTake, $this->pull_request_id); | ||||||
|         $this->application = $application; |         $this->application = $application; | ||||||
|         $this->deployments = $deployments; |         $this->deployments = $deployments; | ||||||
|         $this->deployments_count = $count; |         $this->deployments_count = $count; | ||||||
|         $this->current_url = url()->current(); |         $this->current_url = url()->current(); | ||||||
|         $this->show_pull_request_only(); |         $this->updateCurrentPage(); | ||||||
|         $this->show_more(); |         $this->showMore(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private function show_pull_request_only() |     private function showMore() | ||||||
|     { |  | ||||||
|         if ($this->pull_request_id) { |  | ||||||
|             $this->deployments = $this->deployments->where('pull_request_id', $this->pull_request_id); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private function show_more() |  | ||||||
|     { |     { | ||||||
|         if ($this->deployments->count() !== 0) { |         if ($this->deployments->count() !== 0) { | ||||||
|             $this->show_next = true; |             $this->showNext = true; | ||||||
|             if ($this->deployments->count() < $this->default_take) { |             if ($this->deployments->count() < $this->defaultTake) { | ||||||
|                 $this->show_next = false; |                 $this->showNext = false; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function reload_deployments() |     public function reloadDeployments() | ||||||
|     { |     { | ||||||
|         $this->load_deployments(); |         $this->loadDeployments(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function previous_page(?int $take = null) |     public function previousPage(?int $take = null) | ||||||
|     { |     { | ||||||
|         if ($take) { |         if ($take) { | ||||||
|             $this->skip = $this->skip - $take; |             $this->skip = $this->skip - $take; | ||||||
|         } |         } | ||||||
|         $this->skip = $this->skip - $this->default_take; |         $this->skip = $this->skip - $this->defaultTake; | ||||||
|         if ($this->skip < 0) { |         if ($this->skip < 0) { | ||||||
|             $this->show_prev = false; |             $this->showPrev = false; | ||||||
|             $this->skip = 0; |             $this->skip = 0; | ||||||
|         } |         } | ||||||
|         $this->load_deployments(); |         $this->updateCurrentPage(); | ||||||
|  |         $this->loadDeployments(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function next_page(?int $take = null) |     public function nextPage(?int $take = null) | ||||||
|     { |     { | ||||||
|         if ($take) { |         if ($take) { | ||||||
|             $this->skip = $this->skip + $take; |             $this->skip = $this->skip + $take; | ||||||
|         } |         } | ||||||
|         $this->show_prev = true; |         $this->showPrev = true; | ||||||
|         $this->load_deployments(); |         $this->updateCurrentPage(); | ||||||
|  |         $this->loadDeployments(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function load_deployments() |     public function loadDeployments() | ||||||
|     { |     { | ||||||
|         ['deployments' => $deployments, 'count' => $count] = $this->application->deployments($this->skip, $this->default_take); |         ['deployments' => $deployments, 'count' => $count] = $this->application->deployments($this->skip, $this->defaultTake, $this->pull_request_id); | ||||||
|         $this->deployments = $deployments; |         $this->deployments = $deployments; | ||||||
|         $this->deployments_count = $count; |         $this->deployments_count = $count; | ||||||
|         $this->show_pull_request_only(); |         $this->showMore(); | ||||||
|         $this->show_more(); |     } | ||||||
|  | 
 | ||||||
|  |     public function updatedPullRequestId($value) | ||||||
|  |     { | ||||||
|  |         // Sanitize and validate the pull request ID
 | ||||||
|  |         if ($value !== null && $value !== '') { | ||||||
|  |             // Check if it's numeric and positive
 | ||||||
|  |             if (! is_numeric($value) || (float) $value <= 0 || (float) $value != (int) $value) { | ||||||
|  |                 $this->pull_request_id = null; | ||||||
|  |                 $this->dispatch('error', 'Invalid Pull Request ID. Please enter a valid positive number.'); | ||||||
|  | 
 | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             // Ensure it's stored as a string representation of a positive integer
 | ||||||
|  |             $this->pull_request_id = (string) (int) $value; | ||||||
|  |         } else { | ||||||
|  |             $this->pull_request_id = null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Reset pagination when filter changes
 | ||||||
|  |         $this->skip = 0; | ||||||
|  |         $this->showPrev = false; | ||||||
|  |         $this->updateCurrentPage(); | ||||||
|  |         $this->loadDeployments(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function clearFilter() | ||||||
|  |     { | ||||||
|  |         $this->pull_request_id = null; | ||||||
|  |         $this->skip = 0; | ||||||
|  |         $this->showPrev = false; | ||||||
|  |         $this->updateCurrentPage(); | ||||||
|  |         $this->loadDeployments(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function updateCurrentPage() | ||||||
|  |     { | ||||||
|  |         $this->currentPage = intval($this->skip / $this->defaultTake) + 1; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function render() |     public function render() | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ namespace App\Livewire\Project\Database; | |||||||
| 
 | 
 | ||||||
| use App\Models\InstanceSettings; | use App\Models\InstanceSettings; | ||||||
| use App\Models\ScheduledDatabaseBackup; | use App\Models\ScheduledDatabaseBackup; | ||||||
|  | use Illuminate\Support\Collection; | ||||||
| use Illuminate\Support\Facades\Auth; | use Illuminate\Support\Facades\Auth; | ||||||
| use Illuminate\Support\Facades\Hash; | use Illuminate\Support\Facades\Hash; | ||||||
| use Livewire\Component; | use Livewire\Component; | ||||||
| @@ -14,7 +15,19 @@ class BackupExecutions extends Component | |||||||
| 
 | 
 | ||||||
|     public $database; |     public $database; | ||||||
| 
 | 
 | ||||||
|     public $executions = []; |     public ?Collection $executions; | ||||||
|  | 
 | ||||||
|  |     public int $executions_count = 0; | ||||||
|  | 
 | ||||||
|  |     public int $skip = 0; | ||||||
|  | 
 | ||||||
|  |     public int $defaultTake = 10; | ||||||
|  | 
 | ||||||
|  |     public bool $showNext = false; | ||||||
|  | 
 | ||||||
|  |     public bool $showPrev = false; | ||||||
|  | 
 | ||||||
|  |     public int $currentPage = 1; | ||||||
| 
 | 
 | ||||||
|     public $setDeletableBackup; |     public $setDeletableBackup; | ||||||
| 
 | 
 | ||||||
| @@ -40,6 +53,20 @@ class BackupExecutions extends Component | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public function cleanupDeleted() | ||||||
|  |     { | ||||||
|  |         if ($this->backup) { | ||||||
|  |             $deletedCount = $this->backup->executions()->where('local_storage_deleted', true)->count(); | ||||||
|  |             if ($deletedCount > 0) { | ||||||
|  |                 $this->backup->executions()->where('local_storage_deleted', true)->delete(); | ||||||
|  |                 $this->refreshBackupExecutions(); | ||||||
|  |                 $this->dispatch('success', "Cleaned up {$deletedCount} backup entries deleted from local storage."); | ||||||
|  |             } else { | ||||||
|  |                 $this->dispatch('info', 'No backup entries found that are deleted from local storage.'); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public function deleteBackup($executionId, $password) |     public function deleteBackup($executionId, $password) | ||||||
|     { |     { | ||||||
|         if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { |         if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { | ||||||
| @@ -85,18 +112,74 @@ class BackupExecutions extends Component | |||||||
| 
 | 
 | ||||||
|     public function refreshBackupExecutions(): void |     public function refreshBackupExecutions(): void | ||||||
|     { |     { | ||||||
|         if ($this->backup && $this->backup->exists) { |         $this->loadExecutions(); | ||||||
|             $this->executions = $this->backup->executions()->get()->toArray(); |     } | ||||||
|         } else { | 
 | ||||||
|             $this->executions = []; |     public function reloadExecutions() | ||||||
|  |     { | ||||||
|  |         $this->loadExecutions(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function previousPage(?int $take = null) | ||||||
|  |     { | ||||||
|  |         if ($take) { | ||||||
|  |             $this->skip = $this->skip - $take; | ||||||
|         } |         } | ||||||
|  |         $this->skip = $this->skip - $this->defaultTake; | ||||||
|  |         if ($this->skip < 0) { | ||||||
|  |             $this->showPrev = false; | ||||||
|  |             $this->skip = 0; | ||||||
|  |         } | ||||||
|  |         $this->updateCurrentPage(); | ||||||
|  |         $this->loadExecutions(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function nextPage(?int $take = null) | ||||||
|  |     { | ||||||
|  |         if ($take) { | ||||||
|  |             $this->skip = $this->skip + $take; | ||||||
|  |         } | ||||||
|  |         $this->showPrev = true; | ||||||
|  |         $this->updateCurrentPage(); | ||||||
|  |         $this->loadExecutions(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function loadExecutions() | ||||||
|  |     { | ||||||
|  |         if ($this->backup && $this->backup->exists) { | ||||||
|  |             ['executions' => $executions, 'count' => $count] = $this->backup->executionsPaginated($this->skip, $this->defaultTake); | ||||||
|  |             $this->executions = $executions; | ||||||
|  |             $this->executions_count = $count; | ||||||
|  |         } else { | ||||||
|  |             $this->executions = collect([]); | ||||||
|  |             $this->executions_count = 0; | ||||||
|  |         } | ||||||
|  |         $this->showMore(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function showMore() | ||||||
|  |     { | ||||||
|  |         if ($this->executions->count() !== 0) { | ||||||
|  |             $this->showNext = true; | ||||||
|  |             if ($this->executions->count() < $this->defaultTake) { | ||||||
|  |                 $this->showNext = false; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function updateCurrentPage() | ||||||
|  |     { | ||||||
|  |         $this->currentPage = intval($this->skip / $this->defaultTake) + 1; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function mount(ScheduledDatabaseBackup $backup) |     public function mount(ScheduledDatabaseBackup $backup) | ||||||
|     { |     { | ||||||
|         $this->backup = $backup; |         $this->backup = $backup; | ||||||
|         $this->database = $backup->database; |         $this->database = $backup->database; | ||||||
|         $this->refreshBackupExecutions(); |         $this->updateCurrentPage(); | ||||||
|  |         $this->loadExecutions(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function server() |     public function server() | ||||||
|   | |||||||
| @@ -836,9 +836,14 @@ class Application extends BaseModel | |||||||
|         return ApplicationDeploymentQueue::where('application_id', $this->id)->where('created_at', '>=', now()->subDays(7))->orderBy('created_at', 'desc')->get(); |         return ApplicationDeploymentQueue::where('application_id', $this->id)->where('created_at', '>=', now()->subDays(7))->orderBy('created_at', 'desc')->get(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function deployments(int $skip = 0, int $take = 10) |     public function deployments(int $skip = 0, int $take = 10, ?string $pullRequestId = null) | ||||||
|     { |     { | ||||||
|         $deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->orderBy('created_at', 'desc'); |         $deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->orderBy('created_at', 'desc'); | ||||||
|  | 
 | ||||||
|  |         if ($pullRequestId) { | ||||||
|  |             $deployments = $deployments->where('pull_request_id', $pullRequestId); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         $count = $deployments->count(); |         $count = $deployments->count(); | ||||||
|         $deployments = $deployments->skip($skip)->take($take)->get(); |         $deployments = $deployments->skip($skip)->take($take)->get(); | ||||||
| 
 | 
 | ||||||
|   | |||||||
| @@ -36,6 +36,18 @@ class ScheduledDatabaseBackup extends BaseModel | |||||||
|         return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get(); |         return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public function executionsPaginated(int $skip = 0, int $take = 10) | ||||||
|  |     { | ||||||
|  |         $executions = $this->hasMany(ScheduledDatabaseBackupExecution::class)->orderBy('created_at', 'desc'); | ||||||
|  |         $count = $executions->count(); | ||||||
|  |         $executions = $executions->skip($skip)->take($take)->get(); | ||||||
|  | 
 | ||||||
|  |         return [ | ||||||
|  |             'count' => $count, | ||||||
|  |             'executions' => $executions, | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public function server() |     public function server() | ||||||
|     { |     { | ||||||
|         if ($this->database) { |         if ($this->database) { | ||||||
|   | |||||||
| @@ -3,31 +3,36 @@ | |||||||
|     <h1>Deployments</h1> |     <h1>Deployments</h1> | ||||||
|     <livewire:project.shared.configuration-checker :resource="$application" /> |     <livewire:project.shared.configuration-checker :resource="$application" /> | ||||||
|     <livewire:project.application.heading :application="$application" /> |     <livewire:project.application.heading :application="$application" /> | ||||||
|     <div class="flex flex-col gap-2 pb-10" |     <div class="flex flex-col gap-2 pb-10" @if (!$skip) wire:poll.5000ms='reloadDeployments' @endif> | ||||||
|         @if (!$skip) wire:poll.5000ms='reload_deployments' @endif> |  | ||||||
|         <div class="flex items-end gap-2"> |         <div class="flex items-end gap-2"> | ||||||
|             <h2>Deployments <span class="text-xs">({{ $deployments_count }})</span></h2> |             <h2>Deployments <span class="text-xs">({{ $deployments_count }})</span></h2> | ||||||
|             @if ($deployments_count > 0) |             @if ($deployments_count > 0) | ||||||
|                 <x-forms.button disabled="{{ !$show_prev }}" wire:click="previous_page('{{ $default_take }}')"> |                 <div class="flex items-center gap-2"> | ||||||
|                     <svg class="w-4 h-4" viewBox="0 0 24 24"> |                     <x-forms.button disabled="{{ !$showPrev }}" wire:click="previousPage('{{ $defaultTake }}')"> | ||||||
|                         <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" |                         <svg class="w-4 h-4" viewBox="0 0 24 24"> | ||||||
|                             stroke-width="2" d="m14 6l-6 6l6 6z" /> |                             <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" | ||||||
|                     </svg> |                                 stroke-width="2" d="m14 6l-6 6l6 6z" /> | ||||||
|                 </x-forms.button> |                         </svg> | ||||||
|                 <x-forms.button disabled="{{ !$show_next }}" wire:click="next_page('{{ $default_take }}')"> |                     </x-forms.button> | ||||||
|                     <svg class="w-4 h-4" viewBox="0 0 24 24"> |                     <span class="text-sm text-gray-600 dark:text-gray-400 px-2"> | ||||||
|                         <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" |                         Page {{ $currentPage }} of {{ ceil($deployments_count / $defaultTake) }} | ||||||
|                             stroke-width="2" d="m10 18l6-6l-6-6z" /> |                     </span> | ||||||
|                     </svg> |                     <x-forms.button disabled="{{ !$showNext }}" wire:click="nextPage('{{ $defaultTake }}')"> | ||||||
|                 </x-forms.button> |                         <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 |             @endif | ||||||
|         </div> |         </div> | ||||||
|         @if ($deployments_count > 0) |         <form class="flex items-end gap-2"> | ||||||
|             <form class="flex items-end gap-2"> |             <x-forms.input id="pull_request_id" type="number" min="1" label="Pull Request Id"></x-forms.input> | ||||||
|                 <x-forms.input id="pull_request_id" label="Pull Request"></x-forms.input> |             <x-forms.button type="submit">Filter</x-forms.button> | ||||||
|                 <x-forms.button type="submit">Filter</x-forms.button> |             @if ($pull_request_id) | ||||||
|             </form> |                 <x-forms.button type="button" wire:click="clearFilter">Clear</x-forms.button> | ||||||
|         @endif |             @endif | ||||||
|  |         </form> | ||||||
|         @forelse ($deployments as $deployment) |         @forelse ($deployments as $deployment) | ||||||
|             <div @class([ |             <div @class([ | ||||||
|                 'p-2 border-l-2 bg-white dark:bg-coolgray-100', |                 'p-2 border-l-2 bg-white dark:bg-coolgray-100', | ||||||
|   | |||||||
| @@ -1,10 +1,36 @@ | |||||||
| <div wire:init='refreshBackupExecutions'> | <div wire:init='refreshBackupExecutions'> | ||||||
|     @isset($backup) |     @isset($backup) | ||||||
|         <div class="flex items-center gap-2"> |         <div class="flex items-center gap-2"> | ||||||
|             <h3 class="py-4">Executions</h3> |             <h3 class="py-4">Executions <span class="text-xs">({{ $executions_count }})</span></h3> | ||||||
|  |             @if ($executions_count > 0) | ||||||
|  |                 <div class="flex items-center gap-2"> | ||||||
|  |                     <x-forms.button disabled="{{ !$showPrev }}" wire:click="previousPage('{{ $defaultTake }}')"> | ||||||
|  |                         <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="text-sm text-gray-600 dark:text-gray-400 px-2"> | ||||||
|  |                         Page {{ $currentPage }} of {{ ceil($executions_count / $defaultTake) }} | ||||||
|  |                     </span> | ||||||
|  |                     <x-forms.button disabled="{{ !$showNext }}" wire:click="nextPage('{{ $defaultTake }}')"> | ||||||
|  |                         <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 | ||||||
|             <x-forms.button wire:click='cleanupFailed'>Cleanup Failed Backups</x-forms.button> |             <x-forms.button wire:click='cleanupFailed'>Cleanup Failed Backups</x-forms.button> | ||||||
|  |             <x-modal-confirmation title="Cleanup Deleted Backup Entries?" buttonTitle="Cleanup Deleted" isErrorButton | ||||||
|  |                 submitAction="cleanupDeleted()"  | ||||||
|  |                 :actions="['This will permanently delete all backup execution entries that are marked as deleted from local storage.', 'This only removes database entries, not actual backup files.']"  | ||||||
|  |                 confirmationText="cleanup deleted backups" | ||||||
|  |                 confirmationLabel="Please confirm by typing 'cleanup deleted backups' below" | ||||||
|  |                 shortConfirmationLabel="Confirmation" /> | ||||||
|         </div> |         </div> | ||||||
|         <div wire:poll.5000ms="refreshBackupExecutions" class="flex flex-col gap-4"> |         <div @if (!$skip) wire:poll.5000ms="refreshBackupExecutions" @endif | ||||||
|  |             class="flex flex-col gap-4"> | ||||||
|             @forelse($executions as $execution) |             @forelse($executions as $execution) | ||||||
|                 <div wire:key="{{ data_get($execution, 'id') }}" @class([ |                 <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', |                     'flex flex-col border-l-2 transition-colors p-4 bg-white dark:bg-coolgray-100 text-black dark:text-white', | ||||||
|   | |||||||
| @@ -20,26 +20,183 @@ | |||||||
|         @else |         @else | ||||||
|             @forelse($database->scheduledBackups as $backup) |             @forelse($database->scheduledBackups as $backup) | ||||||
|                 @if ($type == 'database') |                 @if ($type == 'database') | ||||||
|                     <a class="box" |                     <a @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 && | ||||||
|  |                             data_get($backup->latest_log, 'status') === 'running', | ||||||
|  |                         'border-error' => | ||||||
|  |                             $backup->latest_log && | ||||||
|  |                             data_get($backup->latest_log, 'status') === 'failed', | ||||||
|  |                         'border-success' => | ||||||
|  |                             $backup->latest_log && | ||||||
|  |                             data_get($backup->latest_log, 'status') === 'success', | ||||||
|  |                         'border-gray-200 dark:border-coolgray-300' => !$backup->latest_log, | ||||||
|  |                     ]) | ||||||
|                         href="{{ route('project.database.backup.execution', [...$parameters, 'backup_uuid' => $backup->uuid]) }}"> |                         href="{{ route('project.database.backup.execution', [...$parameters, 'backup_uuid' => $backup->uuid]) }}"> | ||||||
|                         <div class="flex flex-col"> |                         @if ($backup->latest_log && data_get($backup->latest_log, 'status') === 'running') | ||||||
|                             <div>Frequency: {{ $backup->frequency }} |                             <div class="absolute top-2 right-2"> | ||||||
|                                 ({{ data_get($backup->server(), 'settings.server_timezone', 'Instance timezone') }}) |                                 <x-loading /> | ||||||
|                             </div> |                             </div> | ||||||
|                             <div>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div> |                         @endif | ||||||
|  |                         <div class="flex items-center gap-2 mb-2"> | ||||||
|  |                             @if ($backup->latest_log) | ||||||
|  |                                 <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 dark:shadow-blue-900/5' => | ||||||
|  |                                         data_get($backup->latest_log, 'status') === 'running', | ||||||
|  |                                     'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200 dark:shadow-red-900/5' => | ||||||
|  |                                         data_get($backup->latest_log, 'status') === 'failed', | ||||||
|  |                                     'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200 dark:shadow-green-900/5' => | ||||||
|  |                                         data_get($backup->latest_log, 'status') === 'success', | ||||||
|  |                                 ])> | ||||||
|  |                                     @php | ||||||
|  |                                         $statusText = match (data_get($backup->latest_log, 'status')) { | ||||||
|  |                                             'success' => 'Success', | ||||||
|  |                                             'running' => 'In Progress', | ||||||
|  |                                             'failed' => 'Failed', | ||||||
|  |                                             default => ucfirst(data_get($backup->latest_log, 'status')), | ||||||
|  |                                         }; | ||||||
|  |                                     @endphp | ||||||
|  |                                     {{ $statusText }} | ||||||
|  |                                 </span> | ||||||
|  |                             @else | ||||||
|  |                                 <span | ||||||
|  |                                     class="px-3 py-1 rounded-md text-xs font-medium tracking-wide shadow-xs bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-200"> | ||||||
|  |                                     No executions yet | ||||||
|  |                                 </span> | ||||||
|  |                             @endif | ||||||
|  |                             <h3 class="font-semibold">{{ $backup->frequency }}</h3> | ||||||
|  |                         </div> | ||||||
|  |                         <div class="text-gray-600 dark:text-gray-400 text-sm"> | ||||||
|  |                             @if ($backup->latest_log) | ||||||
|  |                                 Started: | ||||||
|  |                                 {{ formatDateInServerTimezone(data_get($backup->latest_log, 'created_at'), $backup->server()) }} | ||||||
|  |                                 @if (data_get($backup->latest_log, 'status') !== 'running') | ||||||
|  |                                     <br>Ended: | ||||||
|  |                                     {{ formatDateInServerTimezone(data_get($backup->latest_log, 'finished_at'), $backup->server()) }} | ||||||
|  |                                     <br>Duration: | ||||||
|  |                                     {{ calculateDuration(data_get($backup->latest_log, 'created_at'), data_get($backup->latest_log, 'finished_at')) }} | ||||||
|  |                                     <br>Finished | ||||||
|  |                                     {{ \Carbon\Carbon::parse(data_get($backup->latest_log, 'finished_at'))->diffForHumans() }} | ||||||
|  |                                 @endif | ||||||
|  |                                 @if ($backup->save_s3) | ||||||
|  |                                     <br>S3 Storage: Enabled | ||||||
|  |                                 @endif | ||||||
|  |                                 @if (data_get($backup->latest_log, 'status') === 'success') | ||||||
|  |                                     @php | ||||||
|  |                                         $size = data_get($backup->latest_log, 'size', 0); | ||||||
|  |                                         $sizeFormatted = | ||||||
|  |                                             $size > 0 ? number_format($size / 1024 / 1024, 2) . ' MB' : 'Unknown'; | ||||||
|  |                                     @endphp | ||||||
|  |                                     <br>Last Backup Size: {{ $sizeFormatted }} | ||||||
|  |                                 @endif | ||||||
|  |                             @else | ||||||
|  |                                 Last Run: Never | ||||||
|  |                                 <br>Total Executions: 0 | ||||||
|  |                                 @if ($backup->save_s3) | ||||||
|  |                                     <br>S3 Storage: Enabled | ||||||
|  |                                 @endif | ||||||
|  |                             @endif | ||||||
|                         </div> |                         </div> | ||||||
|                     </a> |                     </a> | ||||||
|                 @else |                 @else | ||||||
|                     <div class="box" wire:click="setSelectedBackup('{{ data_get($backup, 'id') }}')"> |                     <div @class([ | ||||||
|                         <div @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-coollabs' => |                         'bg-gray-200 dark:bg-coolgray-200' => | ||||||
|                                 data_get($backup, 'id') === data_get($selectedBackup, 'id'), |                             data_get($backup, 'id') === data_get($selectedBackup, 'id'), | ||||||
|                             'flex flex-col border-l-2 border-transparent', |                         'border-blue-500/50 border-dashed' => | ||||||
|                         ])> |                             $backup->latest_log && | ||||||
|                             <div>Frequency: {{ $backup->frequency }} |                             data_get($backup->latest_log, 'status') === 'running', | ||||||
|                                 ({{ data_get($backup->server(), 'settings.server_timezone', 'Instance timezone') }}) |                         'border-error' => | ||||||
|  |                             $backup->latest_log && | ||||||
|  |                             data_get($backup->latest_log, 'status') === 'failed', | ||||||
|  |                         'border-success' => | ||||||
|  |                             $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') }}')"> | ||||||
|  |                         @if ($backup->latest_log && data_get($backup->latest_log, 'status') === 'running') | ||||||
|  |                             <div class="absolute top-2 right-2"> | ||||||
|  |                                 <x-loading /> | ||||||
|                             </div> |                             </div> | ||||||
|                             <div>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div> |                         @endif | ||||||
|  |                         <div class="flex items-center gap-2 mb-2"> | ||||||
|  |                             @if ($backup->latest_log) | ||||||
|  |                                 <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 dark:shadow-blue-900/5' => | ||||||
|  |                                         data_get($backup->latest_log, 'status') === 'running', | ||||||
|  |                                     'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200 dark:shadow-red-900/5' => | ||||||
|  |                                         data_get($backup->latest_log, 'status') === 'failed', | ||||||
|  |                                     'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200 dark:shadow-green-900/5' => | ||||||
|  |                                         data_get($backup->latest_log, 'status') === 'success', | ||||||
|  |                                 ])> | ||||||
|  |                                     @php | ||||||
|  |                                         $statusText = match (data_get($backup->latest_log, 'status')) { | ||||||
|  |                                             'success' => 'Success', | ||||||
|  |                                             'running' => 'In Progress', | ||||||
|  |                                             'failed' => 'Failed', | ||||||
|  |                                             default => ucfirst(data_get($backup->latest_log, 'status')), | ||||||
|  |                                         }; | ||||||
|  |                                     @endphp | ||||||
|  |                                     {{ $statusText }} | ||||||
|  |                                 </span> | ||||||
|  |                             @else | ||||||
|  |                                 <span | ||||||
|  |                                     class="px-3 py-1 rounded-md text-xs font-medium tracking-wide shadow-xs bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-200"> | ||||||
|  |                                     No executions yet | ||||||
|  |                                 </span> | ||||||
|  |                             @endif | ||||||
|  |                             <h3 class="font-semibold">{{ $backup->frequency }} Backup</h3> | ||||||
|  |                         </div> | ||||||
|  |                         <div class="text-gray-600 dark:text-gray-400 text-sm"> | ||||||
|  |                             @if ($backup->latest_log) | ||||||
|  |                                 Started: | ||||||
|  |                                 {{ formatDateInServerTimezone(data_get($backup->latest_log, 'created_at'), $backup->server()) }} | ||||||
|  |                                 @if (data_get($backup->latest_log, 'status') !== 'running') | ||||||
|  |                                     <br>Ended: | ||||||
|  |                                     {{ formatDateInServerTimezone(data_get($backup->latest_log, 'finished_at'), $backup->server()) }} | ||||||
|  |                                     <br>Duration: | ||||||
|  |                                     {{ calculateDuration(data_get($backup->latest_log, 'created_at'), data_get($backup->latest_log, 'finished_at')) }} | ||||||
|  |                                     <br>Finished | ||||||
|  |                                     {{ \Carbon\Carbon::parse(data_get($backup->latest_log, 'finished_at'))->diffForHumans() }} | ||||||
|  |                                 @endif | ||||||
|  |                                 <br><br>Total Executions: {{ $backup->executions()->count() }} | ||||||
|  |                                 @if ($backup->save_s3) | ||||||
|  |                                     <br>S3 Storage: Enabled | ||||||
|  |                                 @endif | ||||||
|  |                                 @php | ||||||
|  |                                     $successCount = $backup->executions()->where('status', 'success')->count(); | ||||||
|  |                                     $totalCount = $backup->executions()->count(); | ||||||
|  |                                     $successRate = $totalCount > 0 ? round(($successCount / $totalCount) * 100) : 0; | ||||||
|  |                                 @endphp | ||||||
|  |                                 @if ($totalCount > 0) | ||||||
|  |                                     <br>Success Rate: <span @class([ | ||||||
|  |                                         'font-medium', | ||||||
|  |                                         'text-green-600' => $successRate >= 80, | ||||||
|  |                                         'text-yellow-600' => $successRate >= 50 && $successRate < 80, | ||||||
|  |                                         'text-red-600' => $successRate < 50, | ||||||
|  |                                     ])>{{ $successRate }}%</span> | ||||||
|  |                                     ({{ $successCount }}/{{ $totalCount }}) | ||||||
|  |                                 @endif | ||||||
|  |                                 @if (data_get($backup->latest_log, 'status') === 'success') | ||||||
|  |                                     @php | ||||||
|  |                                         $size = data_get($backup->latest_log, 'size', 0); | ||||||
|  |                                         $sizeFormatted = | ||||||
|  |                                             $size > 0 ? number_format($size / 1024 / 1024, 2) . ' MB' : 'Unknown'; | ||||||
|  |                                     @endphp | ||||||
|  |                                     <br>Last Backup Size: {{ $sizeFormatted }} | ||||||
|  |                                 @endif | ||||||
|  |                             @else | ||||||
|  |                                 Last Run: Never | ||||||
|  |                                 <br>Total Executions: 0 | ||||||
|  |                                 @if ($backup->save_s3) | ||||||
|  |                                     <br>S3 Storage: Enabled | ||||||
|  |                                 @endif | ||||||
|  |                             @endif | ||||||
|                         </div> |                         </div> | ||||||
|                     </div> |                     </div> | ||||||
|                 @endif |                 @endif | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Andras Bacsai
					Andras Bacsai