mirror of
https://github.com/coollabsio/coolify.git
synced 2026-03-11 08:55:47 +00:00
- Rearranged the table header to display 'Application' and 'Environment' in a more logical order. - Made the 'Application' and 'Environment' sections clickable, linking to their respective configuration and resource pages. - Removed unnecessary status time display and streamlined the deployment information presentation. - Enhanced the overall structure for better readability and user experience.
377 lines
14 KiB
PHP
377 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Deployment;
|
|
|
|
use App\Enums\ApplicationDeploymentStatus;
|
|
use App\Models\Application;
|
|
use App\Models\ApplicationDeploymentQueue;
|
|
use App\Models\GithubApp;
|
|
use App\Models\GitlabApp;
|
|
use App\Models\Project;
|
|
use App\Models\Server;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Livewire\Component;
|
|
use Livewire\WithPagination;
|
|
|
|
class Index extends Component
|
|
{
|
|
use WithPagination;
|
|
|
|
public ?string $selectedProjectId = null;
|
|
|
|
public ?int $selectedServerId = null;
|
|
|
|
public ?string $selectedApplicationId = null;
|
|
|
|
public ?int $selectedSourceId = null;
|
|
|
|
public ?string $selectedSourceType = null;
|
|
|
|
public ?string $selectedStatus = null;
|
|
|
|
public int $perPage = 20;
|
|
|
|
protected $queryString = [
|
|
'selectedProjectId' => ['except' => ''],
|
|
'selectedServerId' => ['except' => ''],
|
|
'selectedApplicationId' => ['except' => ''],
|
|
'selectedSourceId' => ['except' => ''],
|
|
'selectedSourceType' => ['except' => ''],
|
|
'selectedStatus' => ['except' => ''],
|
|
];
|
|
|
|
public function mount()
|
|
{
|
|
// Initialize filters from query string if present
|
|
}
|
|
|
|
public function loadDeployments(): LengthAwarePaginator
|
|
{
|
|
$teamId = currentTeam()->id;
|
|
|
|
// Use explicit joins with type casting because application_deployment_queues.application_id
|
|
// is stored as a string (VARCHAR) but applications.id is a bigint, causing PostgreSQL
|
|
// type mismatch errors when using whereHas relationships
|
|
$query = ApplicationDeploymentQueue::query()
|
|
->join('applications', function ($join) {
|
|
$join->on(DB::raw('CAST(application_deployment_queues.application_id AS INTEGER)'), '=', 'applications.id');
|
|
})
|
|
->join('environments', 'applications.environment_id', '=', 'environments.id')
|
|
->join('projects', 'environments.project_id', '=', 'projects.id')
|
|
->where('projects.team_id', $teamId)
|
|
->whereNull('applications.deleted_at')
|
|
->select('application_deployment_queues.*');
|
|
|
|
// Filter by project
|
|
if ($this->selectedProjectId) {
|
|
$query->where('projects.uuid', $this->selectedProjectId);
|
|
}
|
|
|
|
// Filter by server
|
|
if ($this->selectedServerId) {
|
|
$query->where('application_deployment_queues.server_id', $this->selectedServerId);
|
|
}
|
|
|
|
// Filter by application
|
|
if ($this->selectedApplicationId) {
|
|
$query->where('applications.uuid', $this->selectedApplicationId);
|
|
}
|
|
|
|
// Filter by source
|
|
if ($this->selectedSourceId && $this->selectedSourceType) {
|
|
$query->where('applications.source_id', $this->selectedSourceId)
|
|
->where('applications.source_type', $this->selectedSourceType);
|
|
}
|
|
|
|
// Filter by status
|
|
if ($this->selectedStatus) {
|
|
$query->where('application_deployment_queues.status', $this->selectedStatus);
|
|
}
|
|
|
|
return $query->with([
|
|
'application.environment.project',
|
|
'application.source',
|
|
])
|
|
->orderBy('application_deployment_queues.created_at', 'desc')
|
|
->paginate($this->perPage);
|
|
}
|
|
|
|
public function updatedSelectedProjectId()
|
|
{
|
|
$this->resetPage();
|
|
}
|
|
|
|
public function updatedSelectedServerId()
|
|
{
|
|
$this->resetPage();
|
|
}
|
|
|
|
public function updatedSelectedApplicationId()
|
|
{
|
|
$this->resetPage();
|
|
}
|
|
|
|
public function updatedSelectedSourceId()
|
|
{
|
|
$this->resetPage();
|
|
}
|
|
|
|
public function updatedSelectedStatus()
|
|
{
|
|
$this->resetPage();
|
|
}
|
|
|
|
public function clearFilters()
|
|
{
|
|
$this->selectedProjectId = null;
|
|
$this->selectedServerId = null;
|
|
$this->selectedApplicationId = null;
|
|
$this->selectedSourceId = null;
|
|
$this->selectedSourceType = null;
|
|
$this->selectedStatus = null;
|
|
$this->resetPage();
|
|
}
|
|
|
|
public function getFilterOptionsProperty(): array
|
|
{
|
|
$teamId = currentTeam()->id;
|
|
|
|
// Get projects available for filtering
|
|
$projects = Project::ownedByCurrentTeamCached()
|
|
->pluck('name', 'uuid')
|
|
->toArray();
|
|
|
|
// Get servers that have deployments (only show servers with actual deployments)
|
|
// Uses same join pattern as loadDeployments() to avoid type mismatch
|
|
$serverIds = ApplicationDeploymentQueue::query()
|
|
->join('applications', function ($join) {
|
|
$join->on(DB::raw('CAST(application_deployment_queues.application_id AS INTEGER)'), '=', 'applications.id');
|
|
})
|
|
->join('environments', 'applications.environment_id', '=', 'environments.id')
|
|
->join('projects', 'environments.project_id', '=', 'projects.id')
|
|
->where('projects.team_id', $teamId)
|
|
->whereNull('applications.deleted_at')
|
|
->distinct('application_deployment_queues.server_id')
|
|
->whereNotNull('application_deployment_queues.server_id')
|
|
->pluck('application_deployment_queues.server_id')
|
|
->toArray();
|
|
|
|
$servers = [];
|
|
if (! empty($serverIds)) {
|
|
$servers = Server::whereIn('id', $serverIds)
|
|
->pluck('name', 'id')
|
|
->toArray();
|
|
}
|
|
|
|
// Get applications that have deployments (only show applications with actual deployments)
|
|
// Uses same join pattern as loadDeployments() to avoid type mismatch
|
|
$applicationUuids = ApplicationDeploymentQueue::query()
|
|
->join('applications', function ($join) {
|
|
$join->on(DB::raw('CAST(application_deployment_queues.application_id AS INTEGER)'), '=', 'applications.id');
|
|
})
|
|
->join('environments', 'applications.environment_id', '=', 'environments.id')
|
|
->join('projects', 'environments.project_id', '=', 'projects.id')
|
|
->where('projects.team_id', $teamId)
|
|
->whereNull('applications.deleted_at')
|
|
->distinct('applications.uuid')
|
|
->pluck('applications.uuid')
|
|
->toArray();
|
|
|
|
$applications = [];
|
|
if (! empty($applicationUuids)) {
|
|
$applications = Application::whereIn('uuid', $applicationUuids)
|
|
->pluck('name', 'uuid')
|
|
->toArray();
|
|
}
|
|
|
|
// Get sources (GitHub/GitLab apps) from applications with deployments
|
|
// Applications use polymorphic relationships for sources, so we need to handle
|
|
// both GithubApp and GitlabApp types
|
|
$sources = [];
|
|
$sourceData = ApplicationDeploymentQueue::query()
|
|
->join('applications', function ($join) {
|
|
$join->on(DB::raw('CAST(application_deployment_queues.application_id AS INTEGER)'), '=', 'applications.id');
|
|
})
|
|
->join('environments', 'applications.environment_id', '=', 'environments.id')
|
|
->join('projects', 'environments.project_id', '=', 'projects.id')
|
|
->where('projects.team_id', $teamId)
|
|
->whereNull('applications.deleted_at')
|
|
->whereNotNull('applications.source_id')
|
|
->whereNotNull('applications.source_type')
|
|
->select('applications.source_id', 'applications.source_type')
|
|
->distinct()
|
|
->get()
|
|
->map(function ($row) {
|
|
// Resolve polymorphic source relationship
|
|
$source = null;
|
|
if ($row->source_type === GithubApp::class) {
|
|
$source = GithubApp::find($row->source_id);
|
|
} elseif ($row->source_type === GitlabApp::class) {
|
|
$source = GitlabApp::find($row->source_id);
|
|
}
|
|
|
|
if (! $source) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'id' => $source->id,
|
|
'type' => $row->source_type,
|
|
'source' => $source,
|
|
];
|
|
})
|
|
->filter()
|
|
// Ensure unique sources by combining type and id
|
|
->unique(function ($item) {
|
|
return $item['type'].'-'.$item['id'];
|
|
});
|
|
|
|
foreach ($sourceData as $item) {
|
|
$source = $item['source'];
|
|
if ($item['type'] === GithubApp::class) {
|
|
$sources[] = [
|
|
'id' => $source->id,
|
|
'type' => GithubApp::class,
|
|
'name' => 'GitHub: '.$source->name,
|
|
];
|
|
} elseif ($item['type'] === GitlabApp::class) {
|
|
$sources[] = [
|
|
'id' => $source->id,
|
|
'type' => GitlabApp::class,
|
|
'name' => 'GitLab: '.$source->name,
|
|
];
|
|
}
|
|
}
|
|
|
|
// Get statuses
|
|
$statuses = [];
|
|
foreach (ApplicationDeploymentStatus::cases() as $status) {
|
|
$statuses[$status->value] = match ($status) {
|
|
ApplicationDeploymentStatus::QUEUED => 'Queued',
|
|
ApplicationDeploymentStatus::IN_PROGRESS => 'In Progress',
|
|
ApplicationDeploymentStatus::FINISHED => 'Finished',
|
|
ApplicationDeploymentStatus::FAILED => 'Failed',
|
|
ApplicationDeploymentStatus::CANCELLED_BY_USER => 'Cancelled',
|
|
};
|
|
}
|
|
|
|
return [
|
|
'projects' => $projects,
|
|
'servers' => $servers,
|
|
'applications' => $applications,
|
|
'sources' => $sources,
|
|
'statuses' => $statuses,
|
|
];
|
|
}
|
|
|
|
public function getShouldShowProjectFilterProperty(): bool
|
|
{
|
|
return count($this->getFilterOptionsProperty()['projects']) > 1;
|
|
}
|
|
|
|
public function getShouldShowServerFilterProperty(): bool
|
|
{
|
|
return count($this->getFilterOptionsProperty()['servers']) > 1;
|
|
}
|
|
|
|
public function getShouldShowApplicationFilterProperty(): bool
|
|
{
|
|
return count($this->getFilterOptionsProperty()['applications']) > 1;
|
|
}
|
|
|
|
public function getShouldShowSourceFilterProperty(): bool
|
|
{
|
|
return count($this->getFilterOptionsProperty()['sources']) > 1;
|
|
}
|
|
|
|
public function reloadDeployments()
|
|
{
|
|
// This method is called by wire:poll to refresh the data
|
|
$this->render();
|
|
}
|
|
|
|
public function previousPage()
|
|
{
|
|
$this->setPage(max(1, $this->getPage() - 1));
|
|
}
|
|
|
|
public function nextPage()
|
|
{
|
|
$deployments = $this->loadDeployments();
|
|
if ($deployments->hasMorePages()) {
|
|
$this->setPage($this->getPage() + 1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine if we should poll for updates.
|
|
* Checks for ANY active deployments across the team, not just the current page,
|
|
* to ensure new deployments are detected even if they're not visible yet.
|
|
*/
|
|
public function getIsPollingProperty(): bool
|
|
{
|
|
$teamId = currentTeam()->id;
|
|
|
|
// Check if there are ANY active deployments in the team (not just current page)
|
|
// This ensures new deployments are detected even if they're not on the current page
|
|
$hasActiveDeployments = ApplicationDeploymentQueue::query()
|
|
->join('applications', function ($join) {
|
|
$join->on(DB::raw('CAST(application_deployment_queues.application_id AS INTEGER)'), '=', 'applications.id');
|
|
})
|
|
->join('environments', 'applications.environment_id', '=', 'environments.id')
|
|
->join('projects', 'environments.project_id', '=', 'projects.id')
|
|
->where('projects.team_id', $teamId)
|
|
->whereNull('applications.deleted_at')
|
|
->whereIn('application_deployment_queues.status', [
|
|
ApplicationDeploymentStatus::QUEUED->value,
|
|
ApplicationDeploymentStatus::IN_PROGRESS->value,
|
|
])
|
|
->exists();
|
|
|
|
return $hasActiveDeployments;
|
|
}
|
|
|
|
/**
|
|
* Get formatted log lines for a deployment.
|
|
* Only returns logs for active deployments (in_progress or queued).
|
|
* Reuses the same log decoding logic from the deployment show page.
|
|
*/
|
|
public function getLogLines(ApplicationDeploymentQueue $deployment): Collection
|
|
{
|
|
// Only show logs for active deployments to avoid unnecessary processing
|
|
if (!in_array($deployment->status, ['in_progress', 'queued'])) {
|
|
return collect();
|
|
}
|
|
|
|
// Decode and format logs using the same helper function as deployment show page
|
|
return decode_remote_command_output($deployment)->map(function ($logLine) {
|
|
// Escape HTML and convert URLs to clickable links
|
|
$logLine['line'] = e($logLine['line']);
|
|
$logLine['line'] = preg_replace(
|
|
'/(https?:\/\/[^\s]+)/',
|
|
'<a href="$1" target="_blank" rel="noopener noreferrer" class="underline text-neutral-400">$1</a>',
|
|
$logLine['line'],
|
|
);
|
|
|
|
return $logLine;
|
|
});
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
$deployments = $this->loadDeployments();
|
|
|
|
return view('livewire.deployment.index', [
|
|
'deployments' => $deployments,
|
|
'filterOptions' => $this->getFilterOptionsProperty(),
|
|
'isPolling' => $this->isPolling,
|
|
'shouldShowProjectFilter' => $this->shouldShowProjectFilter,
|
|
'shouldShowServerFilter' => $this->shouldShowServerFilter,
|
|
'shouldShowApplicationFilter' => $this->shouldShowApplicationFilter,
|
|
'shouldShowSourceFilter' => $this->shouldShowSourceFilter,
|
|
]);
|
|
}
|
|
}
|
|
|