mirror of
https://github.com/coollabsio/coolify.git
synced 2026-03-11 08:55:47 +00:00
deployments tab
This commit is contained in:
parent
f6a59fa2dc
commit
b38f89cf06
4 changed files with 496 additions and 0 deletions
271
app/Livewire/Deployments/Index.php
Normal file
271
app/Livewire/Deployments/Index.php
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Deployments;
|
||||
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\Url;
|
||||
use Livewire\Component;
|
||||
|
||||
class Index extends Component
|
||||
{
|
||||
#[Url]
|
||||
public ?string $status = null;
|
||||
|
||||
#[Url]
|
||||
public ?int $server_id = null;
|
||||
|
||||
#[Url]
|
||||
public ?int $project_id = null;
|
||||
|
||||
#[Url]
|
||||
public ?int $application_id = null;
|
||||
|
||||
public int $skip = 0;
|
||||
|
||||
public int $defaultTake = 10;
|
||||
|
||||
public bool $showNext = false;
|
||||
|
||||
public bool $showPrev = false;
|
||||
|
||||
public int $currentPage = 1;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},ApplicationDeploymentQueued" => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->loadDeployments();
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function servers(): Collection
|
||||
{
|
||||
return Server::ownedByCurrentTeamCached();
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function projects(): Collection
|
||||
{
|
||||
return Project::ownedByCurrentTeamCached();
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function applications(): Collection
|
||||
{
|
||||
$query = Application::query()
|
||||
->whereHas('environment.project', function ($q) {
|
||||
$q->where('team_id', currentTeam()->id);
|
||||
});
|
||||
|
||||
if ($this->project_id) {
|
||||
$query->whereHas('environment', function ($q) {
|
||||
$q->where('project_id', $this->project_id);
|
||||
});
|
||||
}
|
||||
|
||||
return $query->orderBy('name')->get();
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function statuses(): array
|
||||
{
|
||||
return [
|
||||
'' => 'All Statuses',
|
||||
ApplicationDeploymentStatus::QUEUED->value => 'Queued',
|
||||
ApplicationDeploymentStatus::IN_PROGRESS->value => 'In Progress',
|
||||
ApplicationDeploymentStatus::FINISHED->value => 'Finished',
|
||||
ApplicationDeploymentStatus::FAILED->value => 'Failed',
|
||||
ApplicationDeploymentStatus::CANCELLED_BY_USER->value => 'Cancelled',
|
||||
];
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function deployments(): Collection
|
||||
{
|
||||
$serverIds = $this->servers->pluck('id');
|
||||
|
||||
$query = ApplicationDeploymentQueue::with(['application.environment.project', 'application.destination.server'])
|
||||
->whereIn('server_id', $serverIds)
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
if ($this->status) {
|
||||
$query->where('status', $this->status);
|
||||
}
|
||||
|
||||
if ($this->server_id) {
|
||||
$query->where('server_id', $this->server_id);
|
||||
}
|
||||
|
||||
if ($this->project_id) {
|
||||
$query->whereHas('application.environment.project', function ($q) {
|
||||
$q->where('id', $this->project_id);
|
||||
});
|
||||
}
|
||||
|
||||
if ($this->application_id) {
|
||||
$query->where('application_id', $this->application_id);
|
||||
}
|
||||
|
||||
return $query->skip($this->skip)->take($this->defaultTake)->get();
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function deploymentsCount(): int
|
||||
{
|
||||
$serverIds = $this->servers->pluck('id');
|
||||
|
||||
$query = ApplicationDeploymentQueue::whereIn('server_id', $serverIds);
|
||||
|
||||
if ($this->status) {
|
||||
$query->where('status', $this->status);
|
||||
}
|
||||
|
||||
if ($this->server_id) {
|
||||
$query->where('server_id', $this->server_id);
|
||||
}
|
||||
|
||||
if ($this->project_id) {
|
||||
$query->whereHas('application.environment.project', function ($q) {
|
||||
$q->where('id', $this->project_id);
|
||||
});
|
||||
}
|
||||
|
||||
if ($this->application_id) {
|
||||
$query->where('application_id', $this->application_id);
|
||||
}
|
||||
|
||||
return $query->count();
|
||||
}
|
||||
|
||||
public function loadDeployments()
|
||||
{
|
||||
$this->showNext = $this->deployments->count() >= $this->defaultTake && $this->deploymentsCount > ($this->skip + $this->defaultTake);
|
||||
$this->showPrev = $this->skip > 0;
|
||||
}
|
||||
|
||||
public function updatedStatus()
|
||||
{
|
||||
$this->resetPagination();
|
||||
}
|
||||
|
||||
public function updatedServerId()
|
||||
{
|
||||
$this->resetPagination();
|
||||
}
|
||||
|
||||
public function updatedProjectId()
|
||||
{
|
||||
$this->application_id = null;
|
||||
$this->resetPagination();
|
||||
}
|
||||
|
||||
public function updatedApplicationId()
|
||||
{
|
||||
$this->resetPagination();
|
||||
}
|
||||
|
||||
public function resetFilters()
|
||||
{
|
||||
$this->status = null;
|
||||
$this->server_id = null;
|
||||
$this->project_id = null;
|
||||
$this->application_id = null;
|
||||
$this->resetPagination();
|
||||
}
|
||||
|
||||
private function resetPagination()
|
||||
{
|
||||
$this->skip = 0;
|
||||
$this->showPrev = false;
|
||||
$this->currentPage = 1;
|
||||
$this->loadDeployments();
|
||||
}
|
||||
|
||||
public function previousPage()
|
||||
{
|
||||
$this->skip = max(0, $this->skip - $this->defaultTake);
|
||||
$this->updateCurrentPage();
|
||||
$this->loadDeployments();
|
||||
}
|
||||
|
||||
public function nextPage()
|
||||
{
|
||||
$this->skip += $this->defaultTake;
|
||||
$this->updateCurrentPage();
|
||||
$this->loadDeployments();
|
||||
}
|
||||
|
||||
private function updateCurrentPage()
|
||||
{
|
||||
$this->currentPage = intval($this->skip / $this->defaultTake) + 1;
|
||||
}
|
||||
|
||||
public function getDeploymentUrl($deployment): ?string
|
||||
{
|
||||
$application = $deployment->application;
|
||||
if (! $application) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$environment = $application->environment;
|
||||
if (! $environment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$project = $environment->project;
|
||||
if (! $project) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return route('project.application.deployment.show', [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'application_uuid' => $application->uuid,
|
||||
'deployment_uuid' => $deployment->deployment_uuid,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getApplicationUrl($deployment): ?string
|
||||
{
|
||||
$application = $deployment->application;
|
||||
if (! $application) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$environment = $application->environment;
|
||||
if (! $environment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$project = $environment->project;
|
||||
if (! $project) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return route('project.application.configuration', [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'application_uuid' => $application->uuid,
|
||||
]);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$this->loadDeployments();
|
||||
|
||||
return view('livewire.deployments.index');
|
||||
}
|
||||
}
|
||||
|
|
@ -148,6 +148,21 @@
|
|||
Servers
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a title="Deployments" {{ wireNavigate() }}
|
||||
class="{{ request()->is('deployments*') ? 'menu-item-active menu-item' : 'menu-item' }}"
|
||||
href="{{ route('deployments.index') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3 -5a9 9 0 0 0 6 -8a3 3 0 0 0 -3 -3a9 9 0 0 0 -8 6a6 6 0 0 0 -5 3" />
|
||||
<path d="M7 14a6 6 0 0 0 -3 6a6 6 0 0 0 6 -3" />
|
||||
<path d="M15 9m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
</svg>
|
||||
Deployments
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a title="Sources" {{ wireNavigate() }}
|
||||
|
|
|
|||
206
resources/views/livewire/deployments/index.blade.php
Normal file
206
resources/views/livewire/deployments/index.blade.php
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
<div>
|
||||
<x-slot:title>Deployments | Coolify</x-slot:title>
|
||||
<h1>Deployments</h1>
|
||||
<div class="subtitle">All deployments across your projects, applications, and servers.</div>
|
||||
|
||||
<div class="flex flex-col gap-4 pb-10" @if ($this->currentPage === 1 && (!$this->status || $this->status === 'in_progress' || $this->status === 'queued')) wire:poll.5000ms @endif>
|
||||
{{-- Filters --}}
|
||||
<div class="flex flex-wrap items-end gap-4 p-4 bg-white border dark:bg-coolgray-100 dark:border-coolgray-200 border-neutral-300 rounded-md">
|
||||
<div class="flex-1 min-w-[150px]">
|
||||
<label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Status</label>
|
||||
<select id="status" wire:model.live="status" class="w-full rounded-md border-gray-300 dark:border-coolgray-200 dark:bg-coolgray-200 dark:text-white shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
|
||||
@foreach ($this->statuses as $value => $label)
|
||||
<option value="{{ $value }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-1 min-w-[150px]">
|
||||
<label for="server_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Server</label>
|
||||
<select id="server_id" wire:model.live="server_id" class="w-full rounded-md border-gray-300 dark:border-coolgray-200 dark:bg-coolgray-200 dark:text-white shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
|
||||
<option value="">All Servers</option>
|
||||
@foreach ($this->servers as $server)
|
||||
<option value="{{ $server->id }}">{{ $server->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-1 min-w-[150px]">
|
||||
<label for="project_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Project</label>
|
||||
<select id="project_id" wire:model.live="project_id" class="w-full rounded-md border-gray-300 dark:border-coolgray-200 dark:bg-coolgray-200 dark:text-white shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
|
||||
<option value="">All Projects</option>
|
||||
@foreach ($this->projects as $project)
|
||||
<option value="{{ $project->id }}">{{ $project->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-1 min-w-[150px]">
|
||||
<label for="application_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Application</label>
|
||||
<select id="application_id" wire:model.live="application_id" class="w-full rounded-md border-gray-300 dark:border-coolgray-200 dark:bg-coolgray-200 dark:text-white shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
|
||||
<option value="">All Applications</option>
|
||||
@foreach ($this->applications as $application)
|
||||
<option value="{{ $application->id }}">{{ $application->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@if ($status || $server_id || $project_id || $application_id)
|
||||
<x-forms.button wire:click="resetFilters">Clear Filters</x-forms.button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Header with pagination --}}
|
||||
<div class="flex items-end gap-2">
|
||||
<h2>Deployments <span class="text-xs">({{ $this->deploymentsCount }})</span></h2>
|
||||
@if ($this->deploymentsCount > 0)
|
||||
<div class="flex items-center gap-2">
|
||||
<x-forms.button disabled="{{ !$showPrev }}" 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="text-sm text-gray-600 dark:text-gray-400 px-2">
|
||||
Page {{ $currentPage }} of {{ max(1, ceil($this->deploymentsCount / $defaultTake)) }}
|
||||
</span>
|
||||
<x-forms.button disabled="{{ !$showNext }}" 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>
|
||||
|
||||
{{-- Deployments list --}}
|
||||
<div class="flex flex-col gap-2">
|
||||
@forelse ($this->deployments as $deployment)
|
||||
@php
|
||||
$deploymentUrl = $this->getDeploymentUrl($deployment);
|
||||
$applicationUrl = $this->getApplicationUrl($deployment);
|
||||
$application = $deployment->application;
|
||||
$server = $deployment->application?->destination?->server ?? \App\Models\Server::find($deployment->server_id);
|
||||
@endphp
|
||||
<div @class([
|
||||
'p-4 border-l-2 bg-white dark:bg-coolgray-100 rounded-r-md',
|
||||
'border-blue-500/50 border-dashed' => $deployment->status === 'in_progress',
|
||||
'border-purple-500/50 border-dashed' => $deployment->status === 'queued',
|
||||
'border-white border-dashed' => $deployment->status === 'cancelled-by-user',
|
||||
'border-error' => $deployment->status === 'failed',
|
||||
'border-success' => $deployment->status === 'finished',
|
||||
])>
|
||||
<div class="flex flex-col gap-2">
|
||||
{{-- Header: Status + Application + Project/Environment --}}
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span @class([
|
||||
'px-3 py-1 rounded-md text-xs font-medium shadow-xs',
|
||||
'bg-blue-100/80 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300' => $deployment->status === 'in_progress',
|
||||
'bg-purple-100/80 text-purple-700 dark:bg-purple-500/20 dark:text-purple-300' => $deployment->status === 'queued',
|
||||
'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200' => $deployment->status === 'failed',
|
||||
'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200' => $deployment->status === 'finished',
|
||||
'bg-gray-100 text-gray-700 dark:bg-gray-600/30 dark:text-gray-300' => $deployment->status === 'cancelled-by-user',
|
||||
])>
|
||||
@php
|
||||
$statusText = match ($deployment->status) {
|
||||
'finished' => 'Success',
|
||||
'in_progress' => 'In Progress',
|
||||
'cancelled-by-user' => 'Cancelled',
|
||||
'queued' => 'Queued',
|
||||
default => ucfirst($deployment->status),
|
||||
};
|
||||
@endphp
|
||||
{{ $statusText }}
|
||||
</span>
|
||||
@if ($application)
|
||||
<a href="{{ $applicationUrl }}" {{ wireNavigate() }} class="font-semibold text-gray-900 dark:text-white hover:underline">
|
||||
{{ $deployment->application_name ?? $application->name }}
|
||||
</a>
|
||||
@if ($application->environment?->project)
|
||||
<span class="text-gray-500 dark:text-gray-400">in</span>
|
||||
<span class="text-gray-600 dark:text-gray-300">
|
||||
{{ $application->environment->project->name }} / {{ $application->environment->name }}
|
||||
</span>
|
||||
@endif
|
||||
@else
|
||||
<span class="font-semibold text-gray-900 dark:text-white">
|
||||
{{ $deployment->application_name ?? 'Unknown Application' }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Server info --}}
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<span class="font-medium">Server:</span> {{ $deployment->server_name ?? $server?->name ?? 'Unknown' }}
|
||||
</div>
|
||||
|
||||
{{-- Timestamps --}}
|
||||
@if ($deployment->status !== 'queued')
|
||||
<div class="text-gray-600 dark:text-gray-400 text-sm">
|
||||
Started: {{ formatDateInServerTimezone($deployment->created_at, $server) }}
|
||||
@if ($deployment->status !== 'in_progress' && $deployment->status !== 'cancelled-by-user' && $deployment->finished_at)
|
||||
<br>Ended: {{ formatDateInServerTimezone($deployment->finished_at, $server) }}
|
||||
<br>Duration: {{ calculateDuration($deployment->created_at, $deployment->finished_at) }}
|
||||
<br>Finished {{ \Carbon\Carbon::parse($deployment->finished_at)->diffForHumans() }}
|
||||
@elseif($deployment->status === 'in_progress')
|
||||
<br>Running for: {{ calculateDuration($deployment->created_at, now()) }}
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Commit info --}}
|
||||
@if ($deployment->commit)
|
||||
<div class="text-gray-600 dark:text-gray-400 text-sm">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="font-medium">Commit:</span>
|
||||
@if ($application && method_exists($application, 'gitCommitLink'))
|
||||
<a href="{{ $application->gitCommitLink($deployment->commit) }}" target="_blank" class="underline">
|
||||
{{ substr($deployment->commit, 0, 7) }}
|
||||
</a>
|
||||
@else
|
||||
<span>{{ substr($deployment->commit, 0, 7) }}</span>
|
||||
@endif
|
||||
@if ($deployment->commitMessage())
|
||||
<span class="text-gray-600 dark:text-gray-400">-</span>
|
||||
<span class="text-gray-600 dark:text-gray-400 truncate max-w-md">
|
||||
{{ Str::limit(Str::before($deployment->commitMessage(), "\n"), 80) }}
|
||||
</span>
|
||||
@endif
|
||||
<span class="bg-gray-200/70 dark:bg-gray-600/20 px-2 py-0.5 rounded-md text-xs text-gray-800 dark:text-gray-100 border border-gray-400/30">
|
||||
@if ($deployment->is_webhook)
|
||||
Webhook
|
||||
@if ($deployment->pull_request_id)
|
||||
| PR #{{ $deployment->pull_request_id }}
|
||||
@endif
|
||||
@elseif ($deployment->pull_request_id)
|
||||
PR #{{ $deployment->pull_request_id }}
|
||||
@elseif ($deployment->rollback === true)
|
||||
Rollback
|
||||
@elseif ($deployment->is_api)
|
||||
API
|
||||
@else
|
||||
Manual
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- View deployment link --}}
|
||||
@if ($deploymentUrl)
|
||||
<div class="mt-1">
|
||||
<a href="{{ $deploymentUrl }}" {{ wireNavigate() }} class="text-sm text-indigo-600 dark:text-indigo-400 hover:underline">
|
||||
View deployment logs →
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="p-4 text-center text-gray-500 dark:text-gray-400 bg-white dark:bg-coolgray-100 rounded-md">
|
||||
No deployments found.
|
||||
@if ($status || $server_id || $project_id || $application_id)
|
||||
<button wire:click="resetFilters" class="text-indigo-600 dark:text-indigo-400 hover:underline ml-1">Clear filters</button>
|
||||
@endif
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -6,6 +6,7 @@ use App\Http\Controllers\UploadController;
|
|||
use App\Livewire\Admin\Index as AdminIndex;
|
||||
use App\Livewire\Boarding\Index as BoardingIndex;
|
||||
use App\Livewire\Dashboard;
|
||||
use App\Livewire\Deployments\Index as DeploymentsIndex;
|
||||
use App\Livewire\Destination\Index as DestinationIndex;
|
||||
use App\Livewire\Destination\Show as DestinationShow;
|
||||
use App\Livewire\ForcePasswordReset;
|
||||
|
|
@ -273,6 +274,9 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
|||
Route::get('/security/patches', Patches::class)->name('server.security.patches')->middleware('can.update.resource');
|
||||
Route::get('/security/terminal-access', TerminalAccess::class)->name('server.security.terminal-access')->middleware('can.update.resource');
|
||||
});
|
||||
|
||||
Route::get('/deployments', DeploymentsIndex::class)->name('deployments.index');
|
||||
|
||||
Route::get('/destinations', DestinationIndex::class)->name('destination.index');
|
||||
Route::get('/destination/{destination_uuid}', DestinationShow::class)->name('destination.show');
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue