Improve project resource UIs: sort domains by DNS failure, stop re-adding www pairs on refresh, lazy-load storage tabs with counts, tighten env-var tables, keep application tabs active across Livewire polls, unify database type labels, and update related CSS/JS and tests.
250 lines
16 KiB
PHP
250 lines
16 KiB
PHP
<div>
|
|
<x-slot:title>{{ data_get_str($application, 'name')->limit(10) }} > Deployments | Coolify</x-slot>
|
|
<livewire:project.shared.configuration-checker :resource="$application" />
|
|
<livewire:project.application.heading :application="$application" wire:key="application-heading-deployment-index" />
|
|
|
|
@php
|
|
$lastPage = max(1, (int) ceil($deployments_count / $defaultTake));
|
|
$firstVisibleRow = $deployments_count === 0 ? 0 : $skip + 1;
|
|
$lastVisibleRow = min($skip + $deployments->count(), $deployments_count);
|
|
$hasActiveFilter = $deploymentFilter !== 'all' || filled($pull_request_id);
|
|
$hasActiveQuery = trim($search) !== '' || $hasActiveFilter;
|
|
@endphp
|
|
|
|
<div class="application-settings-form"
|
|
@if (!$skip) wire:poll.5000ms="reloadDeployments" @endif>
|
|
<x-application.settings-section title="Deployment history"
|
|
helper="Search, filter, and open a deployment to inspect its build logs." flush>
|
|
<div
|
|
class="flex flex-wrap items-center gap-2 border-b border-neutral-200 p-3 dark:border-white/[0.08]">
|
|
<div class="relative min-w-0 max-w-md flex-1">
|
|
<input type="search" placeholder="Search deployments" aria-label="Search deployments"
|
|
wire:model.live.debounce.300ms="search" class="input w-full pl-8!" />
|
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-2.5">
|
|
<x-reicon name="search" wire:loading.remove wire:target="search"
|
|
class="size-3.5 text-neutral-400 dark:text-fg-faint" />
|
|
<svg wire:loading wire:target="search" aria-hidden="true"
|
|
class="size-3.5 animate-spin text-neutral-400 dark:text-fg-dim" fill="none"
|
|
viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10"
|
|
stroke="currentColor" stroke-width="4" />
|
|
<path class="opacity-75" fill="currentColor"
|
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ml-auto flex items-center gap-2">
|
|
<div class="relative" x-data="{ open: false }" @keydown.escape.window="open = false">
|
|
<button type="button" class="button" @click="open = !open"
|
|
@click.outside="open = false" aria-haspopup="listbox" :aria-expanded="open">
|
|
<x-reicon name="filter" class="size-3.5" />
|
|
Filter
|
|
</button>
|
|
<div class="listbox-panel left-auto! right-0! z-[90]! w-52! min-w-52!" x-show="open"
|
|
x-cloak role="listbox">
|
|
<button type="button" class="listbox-option" role="option"
|
|
aria-selected="{{ !$hasActiveFilter ? 'true' : 'false' }}"
|
|
wire:click="clearFilter" @click="open = false">
|
|
<span>All deployments</span>
|
|
@if (!$hasActiveFilter)
|
|
<svg class="size-3.5 shrink-0" viewBox="0 0 24 24" fill="none">
|
|
<path d="m4.5 12.75 6 6 9-13.5" stroke="currentColor"
|
|
stroke-width="2.5" stroke-linecap="round"
|
|
stroke-linejoin="round" />
|
|
</svg>
|
|
@endif
|
|
</button>
|
|
|
|
@if (count($statusFilterOptions) > 0)
|
|
<span
|
|
class="px-2 pb-1 pt-2 text-[10px] font-medium uppercase tracking-wider text-neutral-400 dark:text-fg-faint">Status</span>
|
|
@foreach ($statusFilterOptions as $option)
|
|
<button type="button" class="listbox-option" role="option"
|
|
aria-selected="{{ $deploymentFilter === $option['value'] ? 'true' : 'false' }}"
|
|
wire:click="setDeploymentFilter('{{ $option['value'] }}')"
|
|
@click="open = false">
|
|
<span>{{ $option['label'] }}</span>
|
|
@if ($deploymentFilter === $option['value'] && !$pull_request_id)
|
|
<svg class="size-3.5 shrink-0" viewBox="0 0 24 24"
|
|
fill="none">
|
|
<path d="m4.5 12.75 6 6 9-13.5" stroke="currentColor"
|
|
stroke-width="2.5" stroke-linecap="round"
|
|
stroke-linejoin="round" />
|
|
</svg>
|
|
@endif
|
|
</button>
|
|
@endforeach
|
|
@endif
|
|
|
|
@if (count($sourceFilterOptions) > 1)
|
|
<span
|
|
class="px-2 pb-1 pt-2 text-[10px] font-medium uppercase tracking-wider text-neutral-400 dark:text-fg-faint">Source</span>
|
|
@foreach ($sourceFilterOptions as $option)
|
|
<button type="button" class="listbox-option" role="option"
|
|
aria-selected="{{ $deploymentFilter === $option['value'] ? 'true' : 'false' }}"
|
|
wire:click="setDeploymentFilter('{{ $option['value'] }}')"
|
|
@click="open = false">
|
|
<span>{{ $option['label'] }}</span>
|
|
@if ($deploymentFilter === $option['value'] && !$pull_request_id)
|
|
<svg class="size-3.5 shrink-0" viewBox="0 0 24 24"
|
|
fill="none">
|
|
<path d="m4.5 12.75 6 6 9-13.5" stroke="currentColor"
|
|
stroke-width="2.5" stroke-linecap="round"
|
|
stroke-linejoin="round" />
|
|
</svg>
|
|
@endif
|
|
</button>
|
|
@endforeach
|
|
@endif
|
|
|
|
@if (count($pullRequestOptions) > 1)
|
|
<span
|
|
class="px-2 pb-1 pt-2 text-[10px] font-medium uppercase tracking-wider text-neutral-400 dark:text-fg-faint">Pull request</span>
|
|
@foreach (array_slice($pullRequestOptions, 1) as $option)
|
|
<button type="button" class="listbox-option" role="option"
|
|
aria-selected="{{ $pull_request_id === $option['value'] ? 'true' : 'false' }}"
|
|
wire:click="setPullRequestFilter('{{ $option['value'] }}')"
|
|
@click="open = false">
|
|
<span>{{ $option['label'] }}</span>
|
|
@if ($pull_request_id === $option['value'])
|
|
<svg class="size-3.5 shrink-0" viewBox="0 0 24 24"
|
|
fill="none">
|
|
<path d="m4.5 12.75 6 6 9-13.5" stroke="currentColor"
|
|
stroke-width="2.5" stroke-linecap="round"
|
|
stroke-linejoin="round" />
|
|
</svg>
|
|
@endif
|
|
</button>
|
|
@endforeach
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<div class="relative" x-data="{ open: false }" @keydown.escape.window="open = false">
|
|
<button type="button" class="button" @click="open = !open"
|
|
@click.outside="open = false" aria-haspopup="listbox" :aria-expanded="open">
|
|
<x-reicon name="sort-direction" class="size-3.5" />
|
|
Sort
|
|
</button>
|
|
<div class="listbox-panel left-auto! right-0! z-[90]! w-44! min-w-44!" x-show="open"
|
|
x-cloak role="listbox">
|
|
@foreach ([
|
|
'newest' => 'Newest first',
|
|
'oldest' => 'Oldest first',
|
|
] as $sortValue => $sortLabel)
|
|
<button type="button" class="listbox-option" role="option"
|
|
aria-selected="{{ $deploymentSort === $sortValue ? 'true' : 'false' }}"
|
|
wire:click="setDeploymentSort('{{ $sortValue }}')"
|
|
@click="open = false">
|
|
<span>{{ $sortLabel }}</span>
|
|
@if ($deploymentSort === $sortValue)
|
|
<svg class="size-3.5 shrink-0" viewBox="0 0 24 24" fill="none">
|
|
<path d="m4.5 12.75 6 6 9-13.5" stroke="currentColor"
|
|
stroke-width="2.5" stroke-linecap="round"
|
|
stroke-linejoin="round" />
|
|
</svg>
|
|
@endif
|
|
</button>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if ($deployments->isNotEmpty())
|
|
<div class="data-table w-full transition-opacity"
|
|
wire:loading.class="opacity-50 pointer-events-none"
|
|
wire:target="goToPage,previousPage,nextPage">
|
|
<div class="data-table-header deployment-table-grid rounded-none!">
|
|
<span>Status</span>
|
|
<span>Source</span>
|
|
<span>Commit</span>
|
|
<span>Started</span>
|
|
<span>Duration</span>
|
|
<span>Server</span>
|
|
</div>
|
|
|
|
@foreach ($deployments as $deployment)
|
|
@php
|
|
$deploymentStatus = data_get($deployment, 'status');
|
|
$statusLabel = match ($deploymentStatus) {
|
|
'finished' => 'Success',
|
|
'in_progress' => 'In progress',
|
|
'cancelled-by-user' => 'Cancelled',
|
|
'queued' => 'Queued',
|
|
default => str($deploymentStatus)->headline()->toString(),
|
|
};
|
|
$statusType = match ($deploymentStatus) {
|
|
'finished' => 'success',
|
|
'in_progress', 'queued' => 'warning',
|
|
'failed' => 'error',
|
|
default => 'neutral',
|
|
};
|
|
$pullRequestId = (int) data_get($deployment, 'pull_request_id', 0);
|
|
$sourceLabel = match (true) {
|
|
(bool) data_get($deployment, 'is_webhook') && $pullRequestId > 0 => 'Webhook · PR #' . $pullRequestId,
|
|
(bool) data_get($deployment, 'is_webhook') => 'Webhook',
|
|
$pullRequestId > 0 => 'Pull request #' . $pullRequestId,
|
|
(bool) data_get($deployment, 'rollback') => 'Rollback',
|
|
(bool) data_get($deployment, 'is_api') => 'API',
|
|
default => 'Manual',
|
|
};
|
|
$duration = match ($deploymentStatus) {
|
|
'queued' => 'Waiting',
|
|
'in_progress' => calculateDuration(data_get($deployment, 'created_at'), now()),
|
|
default => data_get($deployment, 'finished_at')
|
|
? calculateDuration(data_get($deployment, 'created_at'), data_get($deployment, 'finished_at'))
|
|
: '-',
|
|
};
|
|
$commitMessage = $deployment->commitMessage()
|
|
? Str::before($deployment->commitMessage(), "\n")
|
|
: null;
|
|
@endphp
|
|
<a wire:key="deployment-{{ data_get($deployment, 'deployment_uuid') }}"
|
|
href="{{ $current_url . '/' . data_get($deployment, 'deployment_uuid') }}"
|
|
{{ wireNavigate() }}
|
|
class="data-table-row deployment-table-grid border-b border-neutral-200 text-[13px] text-neutral-600 dark:border-white/[0.08] dark:text-fg-dim">
|
|
<span><x-status-badge :status="$statusLabel" :type="$statusType" /></span>
|
|
<span>{{ $sourceLabel }}</span>
|
|
<span class="min-w-0">
|
|
@if (data_get($deployment, 'commit'))
|
|
<span class="flex min-w-0 items-center gap-2">
|
|
<span class="shrink-0 font-mono text-xs text-neutral-950 dark:text-fg">
|
|
{{ substr(data_get($deployment, 'commit'), 0, 7) }}
|
|
</span>
|
|
@if ($commitMessage)
|
|
<span class="truncate text-neutral-500 dark:text-fg-faint"
|
|
title="{{ $commitMessage }}">{{ $commitMessage }}</span>
|
|
@endif
|
|
</span>
|
|
@else
|
|
<span class="text-neutral-400 dark:text-fg-faint">-</span>
|
|
@endif
|
|
</span>
|
|
<span title="{{ formatDateInServerTimezone(data_get($deployment, 'created_at'), data_get($application, 'destination.server')) }}">
|
|
{{ \Carbon\Carbon::parse(data_get($deployment, 'created_at'))->diffForHumans() }}
|
|
</span>
|
|
<span class="tabular-nums">{{ $duration }}</span>
|
|
<span class="truncate">
|
|
{{ data_get($deployment, 'server_name') ?: data_get($application, 'destination.server.name', '-') }}
|
|
</span>
|
|
</a>
|
|
@endforeach
|
|
|
|
<x-table-pagination :from="$firstVisibleRow" :to="$lastVisibleRow" :total="$deployments_count"
|
|
:current-page="$currentPage" :last-page="$lastPage"
|
|
wire-target="goToPage,previousPage,nextPage" first-action="goToPage(1)"
|
|
previous-action="previousPage" next-action="nextPage"
|
|
last-action="goToPage({{ $lastPage }})" />
|
|
</div>
|
|
@else
|
|
<x-empty size="sm" title="No deployments found"
|
|
:description="$hasActiveQuery
|
|
? 'No deployments match the current search and filters.'
|
|
: 'Deploy the application to create its first deployment record.'"
|
|
icon-name="layers" />
|
|
@endif
|
|
</x-application.settings-section>
|
|
</div>
|
|
</div>
|