mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-14 10:18:20 +00:00
feat(ui): unify resource headings with responsive action menus
Add responsive overflow handling and dedicated action menus for application, service, database, and server headings, while improving upgrade and email settings flows and returning 404s for missing deployment environments.
This commit is contained in:
1 parent
592b537c61
commit
5f3108b68d
37 files changed
+1155
-733
No files matched your search
@@ -398,6 +398,12 @@ html[data-theme="custom"] .animate-spin {
|
||||
color: var(--theme-bright-color) !important;
|
||||
}
|
||||
|
||||
/* Opt out of the brand spinner when the surrounding surface is a selected/neutral control. */
|
||||
.dark .animate-spin.spinner-current,
|
||||
html[data-theme="custom"] .animate-spin.spinner-current {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
html[data-theme="custom"] #nprogress .bar {
|
||||
background: var(--theme-bright-color) !important;
|
||||
}
|
||||
@@ -1710,6 +1716,58 @@ html[data-theme="custom"] textarea:disabled {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.resource-heading-overflow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.resource-heading-overflow-items {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.resource-heading-overflow-items.is-measuring {
|
||||
display: flex !important;
|
||||
visibility: hidden !important;
|
||||
position: absolute !important;
|
||||
inset: auto auto 0 0 !important;
|
||||
flex-direction: row !important;
|
||||
width: max-content !important;
|
||||
height: auto !important;
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
overflow: visible !important;
|
||||
pointer-events: none !important;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.resource-heading-overflow-separator {
|
||||
width: 1px;
|
||||
align-self: stretch;
|
||||
margin: 0.25rem 0.25rem;
|
||||
background: color-mix(in srgb, var(--coollabs-line) 80%, transparent);
|
||||
}
|
||||
|
||||
.resource-heading-overflow.is-collapsed .resource-heading-overflow-separator {
|
||||
width: auto;
|
||||
height: 1px;
|
||||
align-self: auto;
|
||||
margin: 0.25rem 0.375rem;
|
||||
}
|
||||
|
||||
.resource-heading-overflow.is-collapsed .resource-heading-overflow-items > .button,
|
||||
.resource-heading-overflow.is-collapsed .resource-heading-overflow-items > a.button {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
height: auto;
|
||||
min-height: 2rem;
|
||||
padding: 0.375rem 0.625rem;
|
||||
}
|
||||
|
||||
/* Heading action group: buttons + dropdown triggers styled like the tabs */
|
||||
.application-heading-actions .button,
|
||||
.application-heading-actions .app-tab,
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{{-- Application Advanced is unused. Deploy (without cache) lives on the Deploy
|
||||
dropdown. Keep this file so Advanced menus stay on the name="grid" icon. --}}
|
||||
@props(['application'])
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
@props(['application'])
|
||||
|
||||
@php
|
||||
$canDeploy = auth()->user()->can('deploy', $application);
|
||||
$isExited = str($application->status)->startsWith('exited');
|
||||
$isSwarm = $application->destination->server->isSwarm();
|
||||
$isCompose = $application->build_pack === 'dockercompose';
|
||||
$withoutCacheAction = $application->status === 'running' ? 'force_deploy_without_cache' : 'deploy(true)';
|
||||
@endphp
|
||||
|
||||
@if ($isExited)
|
||||
@if ($isSwarm)
|
||||
<button type="button" class="button" @disabled(! $canDeploy) wire:click="deploy">
|
||||
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
||||
Deploy
|
||||
</button>
|
||||
@else
|
||||
<div class="relative" x-data="{ open: false }"
|
||||
x-effect="$dispatch('resource-actions-toggled', { open })"
|
||||
@click.outside="open = false" @keydown.escape.window="open = false">
|
||||
<button type="button" class="button" @click="open = !open" :aria-expanded="open"
|
||||
aria-haspopup="menu" @disabled(! $canDeploy)>
|
||||
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
||||
Deploy
|
||||
<span class="inline-flex transition-transform" :class="open && 'rotate-180'">
|
||||
<x-reicon name="chevron-down" class="size-3 opacity-55" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div x-cloak x-show="open" x-transition.origin.top.right
|
||||
class="listbox-panel top-full! right-0! left-auto! mt-1! w-60! min-w-0!" role="menu">
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(! $canDeploy) wire:click="deploy" @click="open = false" role="menuitem">
|
||||
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
||||
Deploy
|
||||
</button>
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(! $canDeploy) wire:click="{{ $withoutCacheAction }}"
|
||||
@click="open = false" role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Deploy (without cache)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@elseif (! $isSwarm)
|
||||
<div class="relative" x-data="{ open: false }"
|
||||
x-effect="$dispatch('resource-actions-toggled', { open })"
|
||||
@click.outside="open = false" @keydown.escape.window="open = false">
|
||||
<button type="button" class="button" @click="open = !open" :aria-expanded="open"
|
||||
aria-haspopup="menu" @disabled(! $canDeploy)>
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Redeploy
|
||||
<span class="inline-flex transition-transform" :class="open && 'rotate-180'">
|
||||
<x-reicon name="chevron-down" class="size-3 opacity-55" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div x-cloak x-show="open" x-transition.origin.top.right
|
||||
class="listbox-panel top-full! right-0! left-auto! mt-1! w-60! min-w-0!" role="menu">
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(! $canDeploy) wire:click="deploy" @click="open = false" role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Deploy
|
||||
</button>
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(! $canDeploy) wire:click="{{ $withoutCacheAction }}"
|
||||
@click="open = false" role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Deploy (without cache)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@elseif (! $isCompose)
|
||||
<button type="button" class="button" @disabled(! $canDeploy) wire:click="deploy">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Update Service
|
||||
</button>
|
||||
@endif
|
||||
@@ -0,0 +1,149 @@
|
||||
{{--
|
||||
Resource header actions: render inline in the top bar, then collapse into
|
||||
an Actions dropdown when the remaining header width cannot fit them.
|
||||
--}}
|
||||
@props([
|
||||
'id' => null,
|
||||
'label' => 'Actions',
|
||||
])
|
||||
|
||||
<div
|
||||
@if (filled($id)) id="{{ $id }}" @endif
|
||||
{{ $attributes->class('resource-heading-overflow relative shrink-0') }}
|
||||
data-resource-heading-overflow
|
||||
x-data="{
|
||||
collapsed: false,
|
||||
open: false,
|
||||
ro: null,
|
||||
onNavigated: null,
|
||||
update() {
|
||||
const needed = this.measureInlineWidth()
|
||||
const available = this.availableWidth()
|
||||
const next = needed > 0 && needed > available
|
||||
if (next === this.collapsed) {
|
||||
return
|
||||
}
|
||||
this.collapsed = next
|
||||
if (!next) {
|
||||
this.open = false
|
||||
}
|
||||
},
|
||||
hudSlot() {
|
||||
return this.$el.closest('#resource-action-hud-slot')
|
||||
?? document.getElementById('resource-action-hud-slot')
|
||||
},
|
||||
headerRow() {
|
||||
return this.hudSlot()?.parentElement ?? null
|
||||
},
|
||||
measureInlineWidth() {
|
||||
const items = this.$refs.items
|
||||
if (!items) {
|
||||
return 0
|
||||
}
|
||||
items.classList.add('is-measuring')
|
||||
const width = items.scrollWidth
|
||||
items.classList.remove('is-measuring')
|
||||
return width
|
||||
},
|
||||
availableWidth() {
|
||||
const row = this.headerRow()
|
||||
const hud = this.hudSlot()
|
||||
if (!row || !hud) {
|
||||
return Infinity
|
||||
}
|
||||
|
||||
let reserved = 0
|
||||
for (const child of row.children) {
|
||||
if (child === hud) {
|
||||
continue
|
||||
}
|
||||
if (child.classList.contains('flex-1') || child.querySelector(':scope .flex-1')) {
|
||||
reserved += 200
|
||||
continue
|
||||
}
|
||||
reserved += child.getBoundingClientRect().width
|
||||
}
|
||||
|
||||
let hudSiblings = 0
|
||||
const cluster = this.$el.parentElement
|
||||
if (cluster) {
|
||||
for (const child of cluster.children) {
|
||||
if (child === this.$el) {
|
||||
continue
|
||||
}
|
||||
hudSiblings += child.getBoundingClientRect().width
|
||||
}
|
||||
}
|
||||
|
||||
return Math.max(0, row.clientWidth - reserved - hudSiblings - 16)
|
||||
},
|
||||
observe() {
|
||||
if (typeof ResizeObserver === 'undefined') {
|
||||
return
|
||||
}
|
||||
if (!this.ro) {
|
||||
this.ro = new ResizeObserver(() => this.update())
|
||||
}
|
||||
const row = this.headerRow()
|
||||
if (row) {
|
||||
this.ro.observe(row)
|
||||
}
|
||||
this.ro.observe(this.$el)
|
||||
},
|
||||
init() {
|
||||
this.onNavigated = () => this.$nextTick(() => {
|
||||
this.observe()
|
||||
this.update()
|
||||
})
|
||||
document.addEventListener('livewire:navigated', this.onNavigated)
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.observe()
|
||||
this.update()
|
||||
requestAnimationFrame(() => {
|
||||
this.observe()
|
||||
this.update()
|
||||
})
|
||||
})
|
||||
},
|
||||
destroy() {
|
||||
this.ro?.disconnect()
|
||||
if (this.onNavigated) {
|
||||
document.removeEventListener('livewire:navigated', this.onNavigated)
|
||||
}
|
||||
},
|
||||
}"
|
||||
x-init="init()"
|
||||
x-effect="$dispatch('resource-actions-toggled', { open })"
|
||||
@click.outside="open = false"
|
||||
@keydown.escape.window="open = false"
|
||||
@resize.window.debounce.50ms="update()"
|
||||
:class="{ 'is-collapsed': collapsed }"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="button resource-heading-overflow-trigger"
|
||||
x-show="collapsed"
|
||||
x-cloak
|
||||
@click="open = !open"
|
||||
:aria-expanded="open"
|
||||
aria-haspopup="menu"
|
||||
>
|
||||
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
||||
{{ $label }}
|
||||
<span class="inline-flex transition-transform" :class="open && 'rotate-180'">
|
||||
<x-reicon name="chevron-down" class="size-3 opacity-55" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
x-ref="items"
|
||||
class="resource-heading-overflow-items"
|
||||
x-show="!collapsed || open"
|
||||
:class="collapsed && 'listbox-panel top-full! right-0! left-auto! mt-1! min-w-52!'"
|
||||
:role="collapsed ? 'menu' : null"
|
||||
:aria-hidden="collapsed && !open"
|
||||
>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,38 @@
|
||||
@props([
|
||||
'serverIp',
|
||||
'traefikDashboardAvailable' => false,
|
||||
])
|
||||
|
||||
<div class="relative" x-data="{ open: false }"
|
||||
x-effect="$dispatch('resource-actions-toggled', { open })"
|
||||
@click.outside="open = false" @keydown.escape.window="open = false">
|
||||
<button type="button" class="button" @click="open = !open" :aria-expanded="open"
|
||||
aria-haspopup="menu">
|
||||
<x-reicon name="grid" class="size-3.5 opacity-70" />
|
||||
Advanced
|
||||
<span class="inline-flex transition-transform" :class="open && 'rotate-180'">
|
||||
<x-reicon name="chevron-down" class="size-3 opacity-55" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div x-cloak x-show="open" x-transition.origin.top.right
|
||||
class="listbox-panel top-full! right-0! left-auto! mt-1! w-60! min-w-0!" role="menu">
|
||||
@if ($traefikDashboardAvailable)
|
||||
<a class="listbox-option justify-start! gap-2.5!" target="_blank"
|
||||
href="http://{{ $serverIp }}:8080" @click="open = false" role="menuitem">
|
||||
<span class="flex size-4 shrink-0 items-center justify-center">
|
||||
<x-reicon name="external-link" class="size-3! opacity-70" />
|
||||
</span>
|
||||
Traefik Dashboard
|
||||
</a>
|
||||
@endif
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
wire:click="checkProxyStatus" wire:loading.attr="disabled"
|
||||
@click="open = false" role="menuitem">
|
||||
<span class="flex size-4 shrink-0 items-center justify-center">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
</span>
|
||||
Refresh Proxy Status
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,49 @@
|
||||
@php
|
||||
$status = str($service->status ?? '');
|
||||
$canDeploy = auth()->user()->can('deploy', $service);
|
||||
$canStop = auth()->user()->can('stop', $service);
|
||||
$hasAdvancedItems = ! $status->contains('running');
|
||||
@endphp
|
||||
|
||||
@if ($hasAdvancedItems)
|
||||
<div class="relative" x-data="{ open: false }"
|
||||
x-effect="$dispatch('resource-actions-toggled', { open })"
|
||||
@click.outside="open = false" @keydown.escape.window="open = false">
|
||||
<button type="button" class="button" @click="open = !open" :aria-expanded="open"
|
||||
aria-haspopup="menu">
|
||||
<x-reicon name="grid" class="size-3.5 opacity-70" />
|
||||
Advanced
|
||||
<span class="inline-flex transition-transform" :class="open && 'rotate-180'">
|
||||
<x-reicon name="chevron-down" class="size-3 opacity-55" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div x-cloak x-show="open" x-transition.origin.top.right
|
||||
class="listbox-panel top-full! right-0! left-auto! mt-1! w-64! min-w-0!" role="menu">
|
||||
@if ($status->contains('degraded'))
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(! $canDeploy)
|
||||
@click="$wire.dispatch('forceDeployEvent'); open = false"
|
||||
role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Force Restart
|
||||
</button>
|
||||
@else
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(! $canDeploy)
|
||||
@click="$wire.dispatch('forceDeployEvent'); open = false"
|
||||
role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Force Deploy
|
||||
</button>
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(! $canStop)
|
||||
@click="$wire.dispatch('cleanupEvent'); open = false"
|
||||
role="menuitem">
|
||||
<x-reicon name="trash" class="size-3.5 opacity-70" />
|
||||
Force Cleanup Containers
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,50 @@
|
||||
@props(['service'])
|
||||
|
||||
@php
|
||||
$status = str($service->status ?? '');
|
||||
$canDeploy = auth()->user()->can('deploy', $service);
|
||||
$isRunning = $status->contains('running');
|
||||
$isDegraded = $status->contains('degraded');
|
||||
@endphp
|
||||
|
||||
@if ($isRunning || $isDegraded)
|
||||
@if ($isRunning)
|
||||
<div class="relative" x-data="{ open: false }"
|
||||
x-effect="$dispatch('resource-actions-toggled', { open })"
|
||||
@click.outside="open = false" @keydown.escape.window="open = false">
|
||||
<button type="button" class="button" @click="open = !open" :aria-expanded="open"
|
||||
aria-haspopup="menu" @disabled(! $canDeploy)>
|
||||
<x-reicon name="restart" class="size-3.5 opacity-70" />
|
||||
Restart
|
||||
<span class="inline-flex transition-transform" :class="open && 'rotate-180'">
|
||||
<x-reicon name="chevron-down" class="size-3 opacity-55" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div x-cloak x-show="open" x-transition.origin.top.right
|
||||
class="listbox-panel top-full! right-0! left-auto! mt-1! w-60! min-w-0!" role="menu">
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(! $canDeploy)
|
||||
@click="open = false; document.getElementById('service-restart-trigger')?.click()"
|
||||
role="menuitem">
|
||||
<x-reicon name="restart" class="size-3.5 opacity-70" />
|
||||
Restart current version
|
||||
</button>
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(! $canDeploy)
|
||||
@click="$wire.dispatch('pullAndRestartEvent'); open = false"
|
||||
role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Pull latest and restart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<button type="button" class="button"
|
||||
@disabled(! $canDeploy)
|
||||
@click="document.getElementById('service-restart-trigger')?.click()">
|
||||
<x-reicon name="restart" class="size-3.5 opacity-70" />
|
||||
Restart
|
||||
</button>
|
||||
@endif
|
||||
@endif
|
||||
@@ -25,14 +25,14 @@
|
||||
<div
|
||||
class="flex min-h-10 items-center justify-center gap-2 border-r border-neutral-200 px-2 text-[11px] font-medium last:border-r-0 dark:border-white/[0.08] sm:px-3"
|
||||
:class="{
|
||||
'bg-coollabs/[0.07] text-coollabs dark:bg-warning/[0.09] dark:text-warning': currentStep === {{ $stepNumber }},
|
||||
'bg-neutral-100 text-neutral-900 dark:bg-white/[0.08] dark:text-fg': currentStep === {{ $stepNumber }},
|
||||
'text-emerald-600 dark:text-emerald-400': currentStep > {{ $stepNumber }},
|
||||
'text-neutral-500 dark:text-fg-dim': currentStep < {{ $stepNumber }}
|
||||
}">
|
||||
<span
|
||||
class="flex size-5 shrink-0 items-center justify-center rounded-full border text-[10px] font-semibold"
|
||||
:class="{
|
||||
'border-coollabs/25 bg-coollabs/10 dark:border-warning/25 dark:bg-warning/15': currentStep === {{ $stepNumber }},
|
||||
'border-neutral-300 bg-white dark:border-white/[0.16] dark:bg-white/[0.1]': currentStep === {{ $stepNumber }},
|
||||
'border-emerald-500/25 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400': currentStep > {{ $stepNumber }},
|
||||
'border-neutral-200 dark:border-white/[0.1]': currentStep < {{ $stepNumber }}
|
||||
}">
|
||||
@@ -40,7 +40,7 @@
|
||||
<x-reicon name="check-circle" class="size-3" />
|
||||
</template>
|
||||
<template x-if="currentStep === {{ $stepNumber }}">
|
||||
<svg class="loading-indicator size-3 animate-spin" xmlns="http://www.w3.org/2000/svg"
|
||||
<svg class="spinner-current size-3 animate-spin text-current" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
|
||||
stroke-width="4"></circle>
|
||||
|
||||
@@ -118,22 +118,18 @@
|
||||
</div>
|
||||
|
||||
<div class="application-settings-form">
|
||||
<x-application.settings-section title="Resend"
|
||||
description="Use Resend as an alternative email delivery provider.">
|
||||
<x-application.settings-section title="Resend">
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<div class="lg:col-span-2">
|
||||
<div class="w-full sm:w-72">
|
||||
<x-forms.listbox id="resendEnabled" label="Resend delivery"
|
||||
onChange="submitResend"
|
||||
:disabled="!auth()->user()->can('update', $settings)" :options="[
|
||||
['value' => true, 'label' => 'Enabled'],
|
||||
['value' => false, 'label' => 'Disabled'],
|
||||
]" />
|
||||
</div>
|
||||
</div>
|
||||
<x-forms.listbox id="resendEnabled" label="Resend delivery"
|
||||
onChange="submitResend"
|
||||
:disabled="!auth()->user()->can('update', $settings)" :options="[
|
||||
['value' => true, 'label' => 'Enabled'],
|
||||
['value' => false, 'label' => 'Disabled'],
|
||||
]" />
|
||||
@can('update', $settings)
|
||||
<x-forms.input canGate="update" :canResource="$settings" required type="password"
|
||||
id="resendApiKey" placeholder="API key" label="API key" />
|
||||
<x-forms.input canGate="update" :canResource="$settings" :required="$resendEnabled"
|
||||
type="password" id="resendApiKey" placeholder="API key" label="API key"
|
||||
autocomplete="new-password" />
|
||||
@else
|
||||
<x-forms.input disabled label="API key" value="Hidden (only admins can view)" />
|
||||
@endcan
|
||||
|
||||
@@ -58,13 +58,13 @@
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
wire:click="deploy" @click="open = false" role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Redeploy
|
||||
Deploy
|
||||
</button>
|
||||
@else
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled
|
||||
role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Redeploy
|
||||
Deploy
|
||||
</button>
|
||||
@endcan
|
||||
@endif
|
||||
@@ -135,13 +135,13 @@
|
||||
wire:click="{{ $application->status === 'running' ? 'force_deploy_without_cache' : 'deploy(true)' }}"
|
||||
@click="open = false" role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Force deploy without cache
|
||||
Deploy (without cache)
|
||||
</button>
|
||||
@else
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled
|
||||
role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Force deploy without cache
|
||||
Deploy (without cache)
|
||||
</button>
|
||||
@endcan
|
||||
@endif
|
||||
@@ -186,96 +186,37 @@
|
||||
<div class="resource-heading-menus shrink-0">
|
||||
<x-applications.links :application="$application" />
|
||||
</div>
|
||||
<div id="application-desktop-actions" class="relative" x-data="{ open: false }"
|
||||
x-effect="$dispatch('resource-actions-toggled', { open })"
|
||||
@click.outside="open = false" @keydown.escape.window="open = false">
|
||||
<button type="button" class="button" @click="open = !open" :aria-expanded="open"
|
||||
aria-haspopup="menu">
|
||||
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
||||
Actions
|
||||
<x-reicon name="chevron-down" class="size-3 opacity-55" />
|
||||
</button>
|
||||
|
||||
<div x-cloak x-show="open" x-transition.origin.top.right
|
||||
class="listbox-panel top-full! right-0! left-auto! mt-1! w-60! min-w-0!" role="menu">
|
||||
@if (str($application->status)->startsWith('exited'))
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(!auth()->user()->can('deploy', $application))
|
||||
wire:click="deploy" @click="open = false" role="menuitem">
|
||||
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
||||
Deploy
|
||||
</button>
|
||||
@else
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
@can('deploy', $application)
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
wire:click="deploy" @click="open = false" role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Redeploy
|
||||
</button>
|
||||
@else
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled>
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Redeploy
|
||||
</button>
|
||||
@endcan
|
||||
@endif
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
@if ($application->destination->server->isSwarm())
|
||||
@can('deploy', $application)
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
wire:click="deploy" @click="open = false" role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Update Service
|
||||
</button>
|
||||
@else
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled>
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Update Service
|
||||
</button>
|
||||
@endcan
|
||||
@else
|
||||
@can('deploy', $application)
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="open = false; document.getElementById('application-mobile-restart-trigger')?.click()"
|
||||
role="menuitem">
|
||||
<x-reicon name="restart" class="size-3.5 opacity-70" />
|
||||
Restart
|
||||
</button>
|
||||
@else
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled>
|
||||
<x-reicon name="restart" class="size-3.5 opacity-70" />
|
||||
Restart
|
||||
</button>
|
||||
@endcan
|
||||
@endif
|
||||
@endif
|
||||
<x-applications.deploy :application="$application" />
|
||||
<x-resource-heading-overflow id="application-desktop-actions">
|
||||
@if (! str($application->status)->startsWith('exited'))
|
||||
@if ($application->build_pack !== 'dockercompose' && ! $application->destination->server->isSwarm())
|
||||
@can('deploy', $application)
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="open = false; document.getElementById('application-mobile-stop-trigger')?.click()"
|
||||
role="menuitem">
|
||||
<x-reicon name="stop-circle" class="size-3.5 text-error" />
|
||||
Stop
|
||||
<button type="button" class="button"
|
||||
@click="open = false; document.getElementById('application-mobile-restart-trigger')?.click()">
|
||||
<x-reicon name="restart" class="size-3.5 opacity-70" />
|
||||
Restart
|
||||
</button>
|
||||
@else
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled>
|
||||
<x-reicon name="stop-circle" class="size-3.5 opacity-70" />
|
||||
Stop
|
||||
<button type="button" class="button" disabled>
|
||||
<x-reicon name="restart" class="size-3.5 opacity-70" />
|
||||
Restart
|
||||
</button>
|
||||
@endcan
|
||||
@endif
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<div class="my-1 border-t border-coolgray-200 dark:border-coolgray-300" role="separator"></div>
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(!auth()->user()->can('deploy', $application))
|
||||
wire:click="{{ $application->status === 'running' ? 'force_deploy_without_cache' : 'deploy(true)' }}"
|
||||
@click="open = false" role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Force deploy without cache
|
||||
@can('deploy', $application)
|
||||
<button type="button" class="button"
|
||||
@click="open = false; document.getElementById('application-mobile-stop-trigger')?.click()">
|
||||
<x-reicon name="stop-circle" class="size-3.5 text-error" />
|
||||
Stop
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<button type="button" class="button" disabled>
|
||||
<x-reicon name="stop-circle" class="size-3.5 opacity-70" />
|
||||
Stop
|
||||
</button>
|
||||
@endcan
|
||||
@endif
|
||||
</x-resource-heading-overflow>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -138,38 +138,28 @@
|
||||
class="resource-heading-navbar application-heading-actions flex w-auto min-w-0 items-center justify-end gap-1 overflow-visible">
|
||||
<div class="resource-heading-actions flex shrink-0 items-center gap-0.5">
|
||||
@if ($database->destination->server->isFunctional())
|
||||
@if (! $databaseStatus->startsWith('exited'))
|
||||
<div id="database-desktop-actions" class="relative" x-data="{ open: false }"
|
||||
x-effect="$dispatch('resource-actions-toggled', { open })"
|
||||
@click.outside="open = false" @keydown.escape.window="open = false">
|
||||
<button type="button" class="button" @click="open = !open" :aria-expanded="open">
|
||||
<x-reicon name="play-circle" class="size-3.5 text-warning" />
|
||||
Actions
|
||||
<x-reicon name="chevron-down" class="size-3 opacity-55" />
|
||||
<x-resource-heading-overflow id="database-desktop-actions">
|
||||
@if (! $databaseStatus->startsWith('exited'))
|
||||
<button type="button" class="button"
|
||||
@disabled(!auth()->user()->can('manage', $database))
|
||||
@click="open = false; document.getElementById('database-restart-trigger')?.click()">
|
||||
<x-reicon name="restart" class="size-3.5 opacity-70" />
|
||||
Restart
|
||||
</button>
|
||||
<div x-cloak x-show="open" x-transition.origin.top.right
|
||||
class="listbox-panel top-full! right-0! left-auto! mt-1! w-52! min-w-0!" role="menu">
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(!auth()->user()->can('manage', $database))
|
||||
@click="open = false; document.getElementById('database-restart-trigger')?.click()">
|
||||
<x-reicon name="restart" class="size-3.5 opacity-70" />
|
||||
Restart
|
||||
</button>
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(!auth()->user()->can('manage', $database))
|
||||
@click="open = false; document.getElementById('database-stop-trigger')?.click()">
|
||||
<x-reicon name="stop" class="size-3.5 text-error" />
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<x-forms.button canGate="manage" :canResource="$database"
|
||||
@click="$wire.dispatch('startEvent')">
|
||||
<x-reicon name="play-circle" class="size-4 opacity-70" />
|
||||
Start
|
||||
</x-forms.button>
|
||||
@endif
|
||||
<button type="button" class="button"
|
||||
@disabled(!auth()->user()->can('manage', $database))
|
||||
@click="open = false; document.getElementById('database-stop-trigger')?.click()">
|
||||
<x-reicon name="stop" class="size-3.5 text-error" />
|
||||
Stop
|
||||
</button>
|
||||
@else
|
||||
<x-forms.button canGate="manage" :canResource="$database"
|
||||
@click="$wire.dispatch('startEvent')">
|
||||
<x-reicon name="play-circle" class="size-4 opacity-70" />
|
||||
Start
|
||||
</x-forms.button>
|
||||
@endif
|
||||
</x-resource-heading-overflow>
|
||||
@else
|
||||
<x-status-badge status="Server unavailable" type="error" />
|
||||
@endif
|
||||
|
||||
@@ -94,15 +94,32 @@
|
||||
@click="open = false; document.getElementById('service-restart-trigger')?.click()"
|
||||
role="menuitem">
|
||||
<x-reicon name="restart" class="size-3.5 opacity-70" />
|
||||
Restart
|
||||
Restart current version
|
||||
</button>
|
||||
@else
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled
|
||||
role="menuitem">
|
||||
<x-reicon name="restart" class="size-3.5 opacity-70" />
|
||||
Restart
|
||||
Restart current version
|
||||
</button>
|
||||
@endcan
|
||||
@if ($serviceStatus->contains('running'))
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(!auth()->user()->can('deploy', $service))
|
||||
@click="$wire.dispatch('pullAndRestartEvent'); open = false"
|
||||
role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Pull latest and restart
|
||||
</button>
|
||||
@else
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(!auth()->user()->can('deploy', $service))
|
||||
@click="$wire.dispatch('forceDeployEvent'); open = false"
|
||||
role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Force Restart
|
||||
</button>
|
||||
@endif
|
||||
@can('stop', $service)
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="open = false; document.getElementById('service-stop-trigger')?.click()"
|
||||
@@ -117,23 +134,6 @@
|
||||
Stop
|
||||
</button>
|
||||
@endcan
|
||||
@if ($serviceStatus->contains('running'))
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(!auth()->user()->can('deploy', $service))
|
||||
@click="$wire.dispatch('pullAndRestartEvent'); open = false"
|
||||
role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Pull Latest Images & Restart
|
||||
</button>
|
||||
@else
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(!auth()->user()->can('deploy', $service))
|
||||
@click="$wire.dispatch('forceDeployEvent'); open = false"
|
||||
role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Force Restart
|
||||
</button>
|
||||
@endif
|
||||
@else
|
||||
@can('deploy', $service)
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@@ -184,68 +184,25 @@
|
||||
<div class="resource-heading-menus shrink-0">
|
||||
<x-services.links :service="$service" />
|
||||
</div>
|
||||
<div id="service-desktop-actions" class="relative" x-data="{ open: false }"
|
||||
x-effect="$dispatch('resource-actions-toggled', { open })"
|
||||
@click.outside="open = false" @keydown.escape.window="open = false">
|
||||
<button type="button" class="button" @click="open = !open" :aria-expanded="open">
|
||||
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
||||
Actions
|
||||
<x-reicon name="chevron-down" class="size-3 opacity-55" />
|
||||
<x-services.advanced :service="$service" />
|
||||
<x-services.restart :service="$service" />
|
||||
<x-resource-heading-overflow id="service-desktop-actions">
|
||||
@if ($serviceStatus->contains('running') || $serviceStatus->contains('degraded'))
|
||||
<button type="button" class="button"
|
||||
@disabled(!auth()->user()->can('stop', $service))
|
||||
@click="open = false; document.getElementById('service-stop-trigger')?.click()">
|
||||
<x-reicon name="stop-circle" class="size-3.5 text-error" />
|
||||
Stop
|
||||
</button>
|
||||
<div x-cloak x-show="open" x-transition.origin.top.right
|
||||
class="listbox-panel top-full! right-0! left-auto! mt-1! w-64! min-w-0!" role="menu">
|
||||
@if ($serviceStatus->contains('running') || $serviceStatus->contains('degraded'))
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(!auth()->user()->can('deploy', $service))
|
||||
@click="open = false; document.getElementById('service-restart-trigger')?.click()">
|
||||
<x-reicon name="restart" class="size-3.5 opacity-70" />
|
||||
Restart
|
||||
</button>
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(!auth()->user()->can('stop', $service))
|
||||
@click="open = false; document.getElementById('service-stop-trigger')?.click()">
|
||||
<x-reicon name="stop-circle" class="size-3.5 text-error" />
|
||||
Stop
|
||||
</button>
|
||||
@else
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(!auth()->user()->can('deploy', $service))
|
||||
@click="deploying = true; $wire.dispatch('startEvent'); open = false">
|
||||
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
||||
Deploy
|
||||
</button>
|
||||
@endif
|
||||
<div class="my-1 border-t border-coolgray-200 dark:border-coolgray-300" role="separator"></div>
|
||||
@if ($serviceStatus->contains('running'))
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(!auth()->user()->can('deploy', $service))
|
||||
@click="$wire.dispatch('pullAndRestartEvent'); open = false">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Pull Latest Images & Restart
|
||||
</button>
|
||||
@elseif ($serviceStatus->contains('degraded'))
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(!auth()->user()->can('deploy', $service))
|
||||
@click="$wire.dispatch('forceDeployEvent'); open = false">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Force Restart
|
||||
</button>
|
||||
@else
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(!auth()->user()->can('deploy', $service))
|
||||
@click="$wire.dispatch('forceDeployEvent'); open = false">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Force Deploy
|
||||
</button>
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@disabled(!auth()->user()->can('stop', $service))
|
||||
@click="$wire.dispatch('cleanupEvent'); open = false">
|
||||
<x-reicon name="trash" class="size-3.5 opacity-70" />
|
||||
Force Cleanup Containers
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<button type="button" class="button"
|
||||
@disabled(!auth()->user()->can('deploy', $service))
|
||||
@click="deploying = true; $wire.dispatch('startEvent'); open = false">
|
||||
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
||||
Deploy
|
||||
</button>
|
||||
@endif
|
||||
</x-resource-heading-overflow>
|
||||
@else
|
||||
<a href="{{ $environmentVariablesUrl }}" {{ wireNavigate() }}
|
||||
aria-label="Open required environment variables">
|
||||
|
||||
@@ -286,67 +286,33 @@
|
||||
|
||||
@if ($server->proxySet())
|
||||
@can('manageProxy', $server)
|
||||
<div id="server-desktop-actions" class="resource-heading-actions relative shrink-0"
|
||||
x-data="{ open: false }" x-effect="$dispatch('resource-actions-toggled', { open })"
|
||||
@click.outside="open = false"
|
||||
@keydown.escape.window="open = false">
|
||||
<button type="button" class="button" @click="open = !open" :aria-expanded="open"
|
||||
aria-haspopup="menu" wire:loading.attr="disabled" wire:loading.class="is-loading"
|
||||
wire:target="checkProxy,startProxy">
|
||||
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
||||
Actions
|
||||
<x-reicon name="chevron-down" class="size-3 opacity-55" />
|
||||
</button>
|
||||
|
||||
<div x-cloak x-show="open" x-transition.origin.top.right
|
||||
class="listbox-panel top-full! right-0! left-auto! mt-1! w-60! min-w-0!" role="menu">
|
||||
@if ($proxyCanBeStopped)
|
||||
@if ($traefikDashboardAvailable)
|
||||
<a class="listbox-option justify-start! gap-2.5!" target="_blank"
|
||||
href="http://{{ $serverIp }}:8080" @click="open = false" role="menuitem">
|
||||
<span class="flex size-4 shrink-0 items-center justify-center">
|
||||
<x-reicon name="external-link" class="size-3! opacity-70" />
|
||||
</span>
|
||||
Traefik Dashboard
|
||||
</a>
|
||||
@endif
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="open = false; document.getElementById('server-mobile-restart-proxy-trigger')?.click()"
|
||||
role="menuitem">
|
||||
<span class="flex size-4 shrink-0 items-center justify-center">
|
||||
<x-reicon name="restart" class="size-3.5 opacity-70" />
|
||||
</span>
|
||||
Restart Proxy
|
||||
</button>
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="open = false; document.getElementById('server-mobile-stop-proxy-trigger')?.click()"
|
||||
role="menuitem">
|
||||
<span class="flex size-4 shrink-0 items-center justify-center">
|
||||
<x-reicon name="stop-circle" class="size-3.5 text-error" />
|
||||
</span>
|
||||
Stop Proxy
|
||||
</button>
|
||||
@else
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="open = false; $wire.dispatch('checkProxyEvent')" role="menuitem">
|
||||
<span class="flex size-4 shrink-0 items-center justify-center">
|
||||
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
||||
</span>
|
||||
Start Proxy
|
||||
</button>
|
||||
@endif
|
||||
<div class="my-1 border-t border-coolgray-200 dark:border-coolgray-300"
|
||||
role="separator"></div>
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
wire:click="checkProxyStatus" wire:loading.attr="disabled"
|
||||
@click="open = false" role="menuitem">
|
||||
<x-server.advanced :server-ip="$serverIp" :traefik-dashboard-available="$traefikDashboardAvailable" />
|
||||
<x-resource-heading-overflow id="server-desktop-actions" class="resource-heading-actions">
|
||||
@if ($proxyCanBeStopped)
|
||||
<button type="button" class="button"
|
||||
@click="open = false; document.getElementById('server-mobile-restart-proxy-trigger')?.click()">
|
||||
<span class="flex size-4 shrink-0 items-center justify-center">
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
<x-reicon name="restart" class="size-3.5 opacity-70" />
|
||||
</span>
|
||||
Refresh Proxy Status
|
||||
Restart Proxy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="button"
|
||||
@click="open = false; document.getElementById('server-mobile-stop-proxy-trigger')?.click()">
|
||||
<span class="flex size-4 shrink-0 items-center justify-center">
|
||||
<x-reicon name="stop-circle" class="size-3.5 text-error" />
|
||||
</span>
|
||||
Stop Proxy
|
||||
</button>
|
||||
@else
|
||||
<button type="button" class="button"
|
||||
@click="open = false; $wire.dispatch('checkProxyEvent')">
|
||||
<span class="flex size-4 shrink-0 items-center justify-center">
|
||||
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
||||
</span>
|
||||
Start Proxy
|
||||
</button>
|
||||
@endif
|
||||
</x-resource-heading-overflow>
|
||||
@endcan
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -5,8 +5,12 @@
|
||||
|
||||
<x-settings.layout>
|
||||
<div class="application-settings-form mx-auto flex w-full max-w-[1180px] min-w-0 flex-col gap-6">
|
||||
{{-- One bar for the whole page. Three stacked bars made Save run
|
||||
submitResend(), which required an API key even when Resend was off. --}}
|
||||
<x-unsaved-bar action="submit"
|
||||
targets="smtpFromName,smtpFromAddress,smtpHost,smtpPort,smtpEncryption,smtpUsername,smtpPassword,smtpTimeout,resendApiKey" />
|
||||
|
||||
<form wire:submit="submit">
|
||||
<x-unsaved-bar action="submit" />
|
||||
<x-application.settings-section title="Sender">
|
||||
<x-slot:actions>
|
||||
@include('livewire.partials.settings-email-send-test')
|
||||
@@ -21,7 +25,6 @@
|
||||
</form>
|
||||
|
||||
<form wire:submit.prevent="submitSmtp">
|
||||
<x-unsaved-bar action="submitSmtp" />
|
||||
<x-application.settings-section title="SMTP server">
|
||||
<div class="grid gap-4 lg:grid-cols-3">
|
||||
<div class="lg:col-span-3">
|
||||
@@ -50,7 +53,6 @@
|
||||
</form>
|
||||
|
||||
<form wire:submit.prevent="submitResend">
|
||||
<x-unsaved-bar action="submitResend" />
|
||||
<x-application.settings-section title="Resend">
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<x-forms.listbox id="resendEnabled" label="Resend delivery"
|
||||
@@ -58,8 +60,8 @@
|
||||
['value' => true, 'label' => 'Enabled'],
|
||||
['value' => false, 'label' => 'Disabled'],
|
||||
]" />
|
||||
<x-forms.input type="password" id="resendApiKey" placeholder="API key" required
|
||||
label="API key" autocomplete="new-password" />
|
||||
<x-forms.input type="password" id="resendApiKey" placeholder="API key"
|
||||
:required="$resendEnabled" label="API key" autocomplete="new-password" />
|
||||
</div>
|
||||
</x-application.settings-section>
|
||||
</form>
|
||||
|
||||
@@ -181,6 +181,7 @@
|
||||
currentVersion: config.currentVersion || '',
|
||||
latestVersion: config.latestVersion || '',
|
||||
serviceDown: false,
|
||||
instanceWentDown: false,
|
||||
devMode: config.devMode || false,
|
||||
simulationInterval: null,
|
||||
|
||||
@@ -272,6 +273,60 @@
|
||||
}) >= 0;
|
||||
},
|
||||
|
||||
isReadyToReload(runningVersion) {
|
||||
if (this.hasReachedTargetVersion(runningVersion, this.latestVersion)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Releases before this header existed (e.g. 4.3.1) still
|
||||
// return a healthy /api/health with no X-Coolify-Version.
|
||||
// Only treat that as done after the instance actually went down.
|
||||
return !runningVersion && this.instanceWentDown;
|
||||
},
|
||||
|
||||
startHealthWatch() {
|
||||
if (this.checkHealthInterval) {
|
||||
return;
|
||||
}
|
||||
this.checkHealthInterval = setInterval(() => {
|
||||
this.probeHealth();
|
||||
}, 2000);
|
||||
},
|
||||
|
||||
probeHealth() {
|
||||
this.healthCheckAttempts++;
|
||||
const elapsedMinutes = Math.floor((Date.now() - this.startTime) / 60000);
|
||||
|
||||
return fetch('/api/health')
|
||||
.then(response => {
|
||||
const runningVersion = response.headers.get('X-Coolify-Version');
|
||||
if (!response.ok) {
|
||||
this.instanceWentDown = true;
|
||||
this.currentStep = 4;
|
||||
this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this.healthCheckAttempts);
|
||||
return;
|
||||
}
|
||||
if (this.isReadyToReload(runningVersion)) {
|
||||
this.showSuccess();
|
||||
return;
|
||||
}
|
||||
if (!this.instanceWentDown && this.currentStep < 4) {
|
||||
return;
|
||||
}
|
||||
if (runningVersion) {
|
||||
this.currentStatus = `Coolify is still on ${runningVersion}. Waiting for ${this.latestVersion}...`;
|
||||
} else {
|
||||
this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this.healthCheckAttempts);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Health check failed:', error);
|
||||
this.instanceWentDown = true;
|
||||
this.currentStep = 4;
|
||||
this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this.healthCheckAttempts);
|
||||
});
|
||||
},
|
||||
|
||||
getReviveStatusMessage(elapsedMinutes, attempts) {
|
||||
if (elapsedMinutes === 0) {
|
||||
return `Waiting for Coolify to come back online... (attempt ${attempts})`;
|
||||
@@ -287,31 +342,9 @@
|
||||
},
|
||||
|
||||
revive() {
|
||||
if (this.checkHealthInterval) return true;
|
||||
this.healthCheckAttempts = 0;
|
||||
this.currentStep = 4;
|
||||
console.log('Checking server\'s health...');
|
||||
this.checkHealthInterval = setInterval(() => {
|
||||
this.healthCheckAttempts++;
|
||||
const elapsedMinutes = Math.floor((Date.now() - this.startTime) / 60000);
|
||||
fetch('/api/health')
|
||||
.then(response => {
|
||||
const runningVersion = response.headers.get('X-Coolify-Version');
|
||||
if (response.ok && this.hasReachedTargetVersion(runningVersion, this.latestVersion)) {
|
||||
this.showSuccess();
|
||||
} else if (response.ok) {
|
||||
this.currentStatus = runningVersion
|
||||
? `Coolify is still on ${runningVersion}. Waiting for ${this.latestVersion}...`
|
||||
: this.getReviveStatusMessage(elapsedMinutes, this.healthCheckAttempts);
|
||||
} else {
|
||||
this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this.healthCheckAttempts);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Health check failed:', error);
|
||||
this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this.healthCheckAttempts);
|
||||
});
|
||||
}, 2000);
|
||||
this.startHealthWatch();
|
||||
},
|
||||
|
||||
showSuccess() {
|
||||
@@ -388,7 +421,9 @@
|
||||
this.currentStep = 1;
|
||||
this.currentStatus = 'Starting upgrade...';
|
||||
this.serviceDown = false;
|
||||
this.instanceWentDown = false;
|
||||
this.livewireFailures = 0;
|
||||
this.startHealthWatch();
|
||||
|
||||
// Poll upgrade status via Livewire
|
||||
this.checkUpgradeStatusInterval = setInterval(async () => {
|
||||
@@ -399,25 +434,30 @@
|
||||
this.currentStep = this.mapStepToUI(data.step);
|
||||
this.currentStatus = data.message;
|
||||
} else if (data.status === 'complete') {
|
||||
if (this.hasReachedTargetVersion(data.running_version || this.currentVersion, this.latestVersion)) {
|
||||
if (this.isReadyToReload(data.running_version)) {
|
||||
this.showSuccess();
|
||||
} else {
|
||||
this.currentStep = 4;
|
||||
this.currentStatus = `Waiting for Coolify ${this.latestVersion} to come online...`;
|
||||
this.revive();
|
||||
}
|
||||
} else if (data.status === 'error') {
|
||||
this.showError(data.message);
|
||||
} else if (data.status === 'none' && this.instanceWentDown) {
|
||||
this.revive();
|
||||
await this.probeHealth();
|
||||
}
|
||||
} catch (error) {
|
||||
this.livewireFailures++;
|
||||
if (this.livewireFailures < 3) {
|
||||
this.currentStatus = 'Lost contact with Coolify, retrying...';
|
||||
this.currentStatus = 'Reconnecting. This is expected during an upgrade...';
|
||||
return;
|
||||
}
|
||||
// Repeated Livewire failures usually mean the instance is restarting
|
||||
console.log('Livewire unavailable, switching to health check mode');
|
||||
if (!this.serviceDown) {
|
||||
this.serviceDown = true;
|
||||
this.instanceWentDown = true;
|
||||
this.currentStep = 4;
|
||||
this.currentStatus = 'Coolify is restarting with the new version...';
|
||||
if (this.checkUpgradeStatusInterval) {
|
||||
|
||||
Reference in new issue
Block a user