Merge remote-tracking branch 'origin/next' into ghe-support-helpers

This commit is contained in:
Andras Bacsai committed 2026-07-03 10:15:29 +02:00
commit b90380f542
477 files changed
+19669 -4970

No files matched your search

+9 -1
View File
@@ -122,7 +122,11 @@
}
@utility button {
@apply flex gap-2 justify-center items-center px-2 h-8 text-sm text-black normal-case rounded-sm border-2 outline-0 cursor-pointer font-medium bg-white border-neutral-200 hover:bg-neutral-100 dark:bg-coolgray-100 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-200 dark:border-coolgray-300 hover:text-black disabled:cursor-not-allowed min-w-fit dark:disabled:text-neutral-600 disabled:border-transparent disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-base;
@apply flex gap-2 justify-center items-center px-2 h-8 text-sm text-black normal-case rounded-sm border-2 outline-0 cursor-pointer font-medium bg-white border-neutral-200 hover:bg-neutral-100 dark:bg-coolgray-100 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-200 dark:border-coolgray-300 hover:text-black disabled:cursor-not-allowed min-w-fit dark:disabled:text-neutral-600 disabled:border-neutral-200 dark:disabled:border-coolgray-300 disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-base;
}
@utility auth-tooltip {
@apply fixed z-[99] px-2.5 py-1.5 text-xs rounded-sm pointer-events-none whitespace-nowrap text-neutral-700 bg-neutral-200 dark:text-neutral-300 dark:bg-coolgray-400;
}
@utility alert-success {
@@ -355,4 +359,8 @@
gap: 0;
margin-inline: auto;
}
.sidebar-collapsed .sidebar-collapsed-label {
display: none;
}
}
@@ -3,50 +3,84 @@
Advanced
</x-slot>
@if ($application->status === 'running')
<x-modal-confirmation title="Confirm Application Force Deployment?" buttonTitle="Force deploy"
submitAction="force_deploy_without_cache" :actions="[
'This application will be force deployed without build cache.',
]" :confirmWithText="false" :confirmWithPassword="false"
step2ButtonText="Confirm">
<x-slot:content>
<div class="dropdown-iteme">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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="M12.983 8.978c3.955 -.182 7.017 -1.446 7.017 -2.978c0 -1.657 -3.582 -3 -8 -3c-1.661 0 -3.204 .19 -4.483 .515m-2.783 1.228c-.471 .382 -.734 .808 -.734 1.257c0 1.22 1.944 2.271 4.734 2.74" />
<path
d="M4 6v6c0 1.657 3.582 3 8 3c.986 0 1.93 -.067 2.802 -.19m3.187 -.82c1.251 -.53 2.011 -1.228 2.011 -1.99v-6" />
<path d="M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4" />
<path d="M3 3l18 18" />
</svg>
Force deploy (without
cache)
</div>
</x-slot:content>
</x-modal-confirmation>
@can('deploy', $application)
<x-modal-confirmation title="Confirm Application Force Deployment?" buttonTitle="Force deploy"
submitAction="force_deploy_without_cache" :actions="[
'This application will be force deployed without build cache.',
]" :confirmWithText="false" :confirmWithPassword="false"
step2ButtonText="Confirm">
<x-slot:content>
<div class="dropdown-iteme">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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="M12.983 8.978c3.955 -.182 7.017 -1.446 7.017 -2.978c0 -1.657 -3.582 -3 -8 -3c-1.661 0 -3.204 .19 -4.483 .515m-2.783 1.228c-.471 .382 -.734 .808 -.734 1.257c0 1.22 1.944 2.271 4.734 2.74" />
<path
d="M4 6v6c0 1.657 3.582 3 8 3c.986 0 1.93 -.067 2.802 -.19m3.187 -.82c1.251 -.53 2.011 -1.228 2.011 -1.99v-6" />
<path d="M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4" />
<path d="M3 3l18 18" />
</svg>
Force deploy (without
cache)
</div>
</x-slot:content>
</x-modal-confirmation>
@else
<div class="dropdown-iteme" data-disabled>
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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="M12.983 8.978c3.955 -.182 7.017 -1.446 7.017 -2.978c0 -1.657 -3.582 -3 -8 -3c-1.661 0 -3.204 .19 -4.483 .515m-2.783 1.228c-.471 .382 -.734 .808 -.734 1.257c0 1.22 1.944 2.271 4.734 2.74" />
<path
d="M4 6v6c0 1.657 3.582 3 8 3c.986 0 1.93 -.067 2.802 -.19m3.187 -.82c1.251 -.53 2.011 -1.228 2.011 -1.99v-6" />
<path d="M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4" />
<path d="M3 3l18 18" />
</svg>
Force deploy (without
cache)
</div>
@endcan
@else
<x-modal-confirmation title="Confirm Application Force Deployment?" buttonTitle="Force deploy"
submitAction="deploy(true)" :actions="[
'This application will be force deployed without build cache.',
]" :confirmWithText="false" :confirmWithPassword="false"
step2ButtonText="Confirm">
<x-slot:content>
<div class="dropdown-item">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" 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="M12.983 8.978c3.955 -.182 7.017 -1.446 7.017 -2.978c0 -1.657 -3.582 -3 -8 -3c-1.661 0 -3.204 .19 -4.483 .515m-2.783 1.228c-.471 .382 -.734 .808 -.734 1.257c0 1.22 1.944 2.271 4.734 2.74" />
<path
d="M4 6v6c0 1.657 3.582 3 8 3c.986 0 1.93 -.067 2.802 -.19m3.187 -.82c1.251 -.53 2.011 -1.228 2.011 -1.99v-6" />
<path d="M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4" />
<path d="M3 3l18 18" />
</svg>
Force deploy (without
cache)
</div>
</x-slot:content>
</x-modal-confirmation>
@can('deploy', $application)
<x-modal-confirmation title="Confirm Application Force Deployment?" buttonTitle="Force deploy"
submitAction="deploy(true)" :actions="[
'This application will be force deployed without build cache.',
]" :confirmWithText="false" :confirmWithPassword="false"
step2ButtonText="Confirm">
<x-slot:content>
<div class="dropdown-item">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" 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="M12.983 8.978c3.955 -.182 7.017 -1.446 7.017 -2.978c0 -1.657 -3.582 -3 -8 -3c-1.661 0 -3.204 .19 -4.483 .515m-2.783 1.228c-.471 .382 -.734 .808 -.734 1.257c0 1.22 1.944 2.271 4.734 2.74" />
<path
d="M4 6v6c0 1.657 3.582 3 8 3c.986 0 1.93 -.067 2.802 -.19m3.187 -.82c1.251 -.53 2.011 -1.228 2.011 -1.99v-6" />
<path d="M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4" />
<path d="M3 3l18 18" />
</svg>
Force deploy (without
cache)
</div>
</x-slot:content>
</x-modal-confirmation>
@else
<div class="dropdown-item" data-disabled>
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" 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="M12.983 8.978c3.955 -.182 7.017 -1.446 7.017 -2.978c0 -1.657 -3.582 -3 -8 -3c-1.661 0 -3.204 .19 -4.483 .515m-2.783 1.228c-.471 .382 -.734 .808 -.734 1.257c0 1.22 1.944 2.271 4.734 2.74" />
<path
d="M4 6v6c0 1.657 3.582 3 8 3c.986 0 1.93 -.067 2.802 -.19m3.187 -.82c1.251 -.53 2.011 -1.228 2.011 -1.99v-6" />
<path d="M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4" />
<path d="M3 3l18 18" />
</svg>
Force deploy (without
cache)
</div>
@endcan
@endif
</x-dropdown>
@@ -11,6 +11,7 @@
'certificateValidUntil' => null,
'isExited' => false,
'showPublicUrlPlaceholder' => false,
'isPasswordHiddenForMember' => false,
])
@php
@@ -18,14 +19,19 @@
@endphp
<div class="flex flex-col gap-2">
<x-forms.input :label="$label . ' URL (internal)'" :helper="$urlHelper" type="password" readonly
wire:model="dbUrl" canGate="update" :canResource="$database" />
@if ($dbUrlPublic)
<x-forms.input :label="$label . ' URL (public)'" :helper="$urlHelper" type="password" readonly
wire:model="dbUrlPublic" canGate="update" :canResource="$database" />
@elseif ($showPublicUrlPlaceholder)
<x-forms.input :label="$label . ' URL (public)'" :helper="$urlHelper" readonly
value="Starting the database will generate this." canGate="update" :canResource="$database" />
@if ($isPasswordHiddenForMember)
<x-forms.input :label="$label . ' URL (internal)'" disabled value="Hidden (only admins can view)" />
<x-forms.input :label="$label . ' URL (public)'" disabled value="Hidden (only admins can view)" />
@else
<x-forms.input :label="$label . ' URL (internal)'" :helper="$urlHelper" type="password" readonly
wire:model="dbUrl" canGate="update" :canResource="$database" />
@if ($dbUrlPublic)
<x-forms.input :label="$label . ' URL (public)'" :helper="$urlHelper" type="password" readonly
wire:model="dbUrlPublic" canGate="update" :canResource="$database" />
@elseif ($showPublicUrlPlaceholder)
<x-forms.input :label="$label . ' URL (public)'" :helper="$urlHelper" readonly
value="Starting the database will generate this." canGate="update" :canResource="$database" />
@endif
@endif
@if ($supportsSsl)
@@ -1,3 +1,24 @@
@if ($authDisabled)
<span class="relative inline-flex"
x-data="{ visible: false, _t: null }"
@mouseenter="_t = setTimeout(() => {
visible = true;
$nextTick(() => requestAnimationFrame(() => {
const tip = $refs.tip;
if (!tip) return;
const r = $el.getBoundingClientRect();
const t = tip.getBoundingClientRect();
let top = r.top - t.height - 6;
let left = r.left;
if (top < 4) top = r.bottom + 6;
if (left + t.width > innerWidth - 8) left = innerWidth - 8 - t.width;
if (left < 4) left = 4;
tip.style.top = top + 'px';
tip.style.left = left + 'px';
}));
}, 300)"
@mouseleave="clearTimeout(_t); visible = false">
@endif
<button @disabled($disabled) {{ $attributes->merge(['class' => $defaultClass]) }}
{{ $attributes->merge(['type' => 'button']) }}
@isset($confirm)
@@ -18,3 +39,9 @@
@endif
@endif
</button>
@if ($authDisabled)
<div x-ref="tip" x-show="visible" x-cloak class="auth-tooltip">
You do not have permission to perform this action.
</div>
</span>
@endif
@@ -2,24 +2,27 @@
<div class="w-full" x-data="{ copied: false, isSecure: window.isSecureContext }">
@if ($label)
<label class="flex gap-1 items-center mb-1 text-sm font-medium">{{ $label }}</label>
<label class="flex gap-1 items-center mb-1 text-sm font-medium text-black dark:text-white">{{ $label }}</label>
@endif
<div class="relative">
<input type="text" value="{{ $text }}" class="input pr-10"
<input type="text" value="{{ $text }}"
class="input pr-11 bg-white dark:bg-coolgray-100 dark:read-only:bg-coolgray-100 dark:read-only:text-white"
readonly
@keydown.prevent @paste.prevent @cut.prevent @drop.prevent
@focus="$event.target.select()">
<button
x-show="isSecure"
@click.prevent="copied = true; navigator.clipboard.writeText({{ Js::from($text) }}); setTimeout(() => copied = false, 1000)"
class="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-gray-400 hover:text-gray-300 transition-colors"
title="Copy to clipboard">
<svg x-show="!copied" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<svg x-show="copied" class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</button>
<button
x-show="isSecure"
@click.prevent="copied = true; navigator.clipboard.writeText({{ Js::from($text) }}); setTimeout(() => copied = false, 1000)"
class="absolute right-2 top-1/2 -translate-y-1/2 rounded-sm p-1.5 text-neutral-500 transition-colors hover:text-neutral-700 focus-visible:ring-2 focus-visible:ring-coollabs focus-visible:ring-offset-2 dark:text-neutral-400 dark:hover:text-white dark:focus-visible:ring-warning dark:focus-visible:ring-offset-base"
title="Copy to clipboard"
aria-label="Copy to clipboard">
<svg x-show="!copied" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<svg x-show="copied" class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</button>
</div>
</div>
@@ -6,6 +6,7 @@
'buttonFullWidth' => false,
'customButton' => null,
'disabled' => false,
'authDisabled' => false,
'dispatchAction' => false,
'submitAction' => 'delete',
'content' => null,
@@ -128,9 +129,13 @@
}
}"
@keydown.escape.window="if (modalOpen) { modalOpen = false; resetModal(); }" :class="{ 'z-40': modalOpen }"
class="relative w-auto h-auto">
@class([
'relative h-auto',
'w-full' => $buttonFullWidth,
'w-full sm:w-auto' => ! $buttonFullWidth,
])>
@if (isset($trigger))
<div @click="modalOpen=true">
<div class="w-full" @click="modalOpen=true">
{{ $trigger }}
</div>
@elseif ($customButton)
@@ -151,11 +156,11 @@
@else
@if ($disabled)
@if ($buttonFullWidth)
<x-forms.button class="w-full" isError disabled wire:target>
<x-forms.button class="w-full" isError disabled :authDisabled="$authDisabled" wire:target>
{{ $buttonTitle }}
</x-forms.button>
@else
<x-forms.button isError disabled wire:target>
<x-forms.button isError disabled :authDisabled="$authDisabled" wire:target>
{{ $buttonTitle }}
</x-forms.button>
@endif
@@ -44,16 +44,6 @@
this.queryTheme();
this.checkZoom();
},
setTheme(type) {
this.theme = type;
localStorage.setItem('theme', type);
this.queryTheme();
},
cycleTheme() {
const themes = ['light', 'system', 'dark'];
const currentIndex = themes.indexOf(this.theme || localStorage.getItem('theme') || 'dark');
this.setTheme(themes[(currentIndex + 1) % themes.length]);
},
queryTheme() {
const darkModePreference = window.matchMedia('(prefers-color-scheme: dark)').matches;
const userSettings = localStorage.getItem('theme') || 'dark';
@@ -375,45 +365,6 @@
<li>
<livewire:settings-dropdown trigger="changelog-sidebar" />
</li>
<li>
<button type="button" @click.stop="cycleTheme()"
:title="`Theme: ${theme === 'system' ? 'System default' : theme}. Click to change.`"
:aria-label="`Theme: ${theme === 'system' ? 'System default' : theme}. Click to change theme.`"
class="menu-item"
:class="collapsed && 'lg:hidden'">
<svg x-show="theme === 'dark'" class="menu-item-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
<svg x-show="theme === 'light'" class="menu-item-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<svg x-show="theme === 'system'" class="menu-item-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<span class="text-left menu-item-label">Theme</span>
</button>
<button type="button" @click.stop="cycleTheme()"
:title="`Theme: ${theme === 'system' ? 'System default' : theme}. Click to change.`"
:aria-label="`Theme: ${theme === 'system' ? 'System default' : theme}. Click to change theme.`"
class="menu-item hidden"
:class="collapsed && 'lg:flex'">
<svg x-show="theme === 'dark'" class="menu-item-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
<svg x-show="theme === 'light'" class="menu-item-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<svg x-show="theme === 'system'" class="menu-item-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</button>
</li>
@if (isInstanceAdmin() && !isCloud())
@persist('upgrade')
<li>
@@ -1,10 +1,12 @@
<div class="sub-menu-wrapper">
<a class="{{ request()->routeIs('server.sentinel') ? 'sub-menu-item menu-item-active' : 'sub-menu-item' }}" {{ wireNavigate() }}
href="{{ route('server.sentinel', $parameters) }}">
<span class="menu-item-label">Configuration</span>
</a>
<a class="{{ request()->routeIs('server.sentinel.logs') ? 'sub-menu-item menu-item-active' : 'sub-menu-item' }}" {{ wireNavigate() }}
href="{{ route('server.sentinel.logs', $parameters) }}">
<span class="menu-item-label">Logs</span>
</a>
@can('viewSentinel', $server)
<a class="{{ request()->routeIs('server.sentinel') ? 'sub-menu-item menu-item-active' : 'sub-menu-item' }}" {{ wireNavigate() }}
href="{{ route('server.sentinel', $parameters) }}">
<span class="menu-item-label">Configuration</span>
</a>
<a class="{{ request()->routeIs('server.sentinel.logs') ? 'sub-menu-item menu-item-active' : 'sub-menu-item' }}" {{ wireNavigate() }}
href="{{ route('server.sentinel.logs', $parameters) }}">
<span class="menu-item-label">Logs</span>
</a>
@endcan
</div>
@@ -3,7 +3,7 @@
Advanced
</x-slot>
@if (str($service->status)->contains('running'))
<div class="dropdown-item" @click="document.getElementById('service-pullAndRestart-trigger')?.click()">
<div class="dropdown-item" @if(!auth()->user()->can('deploy', $service)) data-disabled @endif @click="document.getElementById('service-pullAndRestart-trigger')?.click()">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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" />
@@ -17,7 +17,7 @@
Pull Latest Images & Restart
</div>
@elseif (str($service->status)->contains('degraded'))
<div class="dropdown-item" @click="document.getElementById('service-forceDeploy-trigger')?.click()">
<div class="dropdown-item" @if(!auth()->user()->can('deploy', $service)) data-disabled @endif @click="document.getElementById('service-forceDeploy-trigger')?.click()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke=""
style="--darkreader-inline-stroke: currentColor;" class="w-6 h-6" stroke-width="2">
@@ -27,7 +27,7 @@
Force Restart
</div>
@else
<div class="dropdown-item" @click="document.getElementById('service-forceDeploy-trigger')?.click()">
<div class="dropdown-item" @if(!auth()->user()->can('deploy', $service)) data-disabled @endif @click="document.getElementById('service-forceDeploy-trigger')?.click()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke=""
style="--darkreader-inline-stroke: currentColor;" class="w-4 h-4" stroke-width="2">
@@ -36,7 +36,7 @@
</svg>
Force Deploy
</div>
<div class="dropdown-item" @click="document.getElementById('service-cleanup-trigger')?.click()">
<div class="dropdown-item" @if(!auth()->user()->can('stop', $service)) data-disabled @endif @click="document.getElementById('service-cleanup-trigger')?.click()">
<svg class="w-4 h-4" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M26 20h-6v-2h6zm4 8h-6v-2h6zm-2-4h-6v-2h6z" />
<path fill="currentColor"
+67 -50
View File
@@ -11,20 +11,23 @@
<section class="-mt-2">
<div class="flex items-center gap-2 pb-2">
<h3>Projects</h3>
@if ($projects->count() > 0)
<x-modal-input buttonTitle="Add" title="New Project">
<x-slot:content>
<button
class="flex items-center justify-center size-4 text-black dark:text-white rounded hover:bg-coolgray-400 dark:hover:bg-coolgray-300 cursor-pointer">
<svg class="size-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
</button>
</x-slot:content>
<livewire:project.add-empty />
</x-modal-input>
@endif
@can('create', App\Models\Project::class)
@if ($projects->count() > 0)
<x-modal-input buttonTitle="Add" title="New Project">
<x-slot:content>
<button
class="flex items-center justify-center size-4 text-black dark:text-white rounded hover:bg-coolgray-400 dark:hover:bg-coolgray-300 cursor-pointer">
<svg class="size-3" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 4.5v15m7.5-7.5h-15" />
</svg>
</button>
</x-slot:content>
<livewire:project.add-empty />
</x-modal-input>
@endif
@endcan
</div>
@if ($projects->count() > 0)
<div class="grid grid-cols-1 gap-4 xl:grid-cols-2">
@@ -64,12 +67,15 @@
@else
<div class="flex flex-col gap-1">
<div class='font-bold dark:text-warning'>No projects found.</div>
<div class="flex items-center gap-1">
<x-modal-input buttonTitle="Add" title="New Project">
<livewire:project.add-empty />
</x-modal-input> your first project or
go to the <a class="underline dark:text-white" href="{{ route('onboarding') }}" {{ wireNavigate() }}>onboarding</a> page.
</div>
@can('create', App\Models\Project::class)
<div class="flex items-center gap-1">
<x-modal-input buttonTitle="Add" title="New Project">
<livewire:project.add-empty />
</x-modal-input> your first project or
go to the <a class="underline dark:text-white" href="{{ route('onboarding') }}"
{{ wireNavigate() }}>onboarding</a> page.
</div>
@endcan
</div>
@endif
</section>
@@ -77,20 +83,23 @@
<section>
<div class="flex items-center gap-2 pb-2">
<h3>Servers</h3>
@if ($servers->count() > 0 && $privateKeys->count() > 0)
<x-modal-input buttonTitle="Add" title="New Server" :closeOutside="false">
<x-slot:content>
<button
class="flex items-center justify-center size-4 text-black dark:text-white rounded hover:bg-coolgray-400 dark:hover:bg-coolgray-300 cursor-pointer">
<svg class="size-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
</button>
</x-slot:content>
<livewire:server.create />
</x-modal-input>
@endif
@can('create', App\Models\Server::class)
@if ($servers->count() > 0 && $privateKeys->count() > 0)
<x-modal-input buttonTitle="Add" title="New Server" :closeOutside="false">
<x-slot:content>
<button
class="flex items-center justify-center size-4 text-black dark:text-white rounded hover:bg-coolgray-400 dark:hover:bg-coolgray-300 cursor-pointer">
<svg class="size-3" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 4.5v15m7.5-7.5h-15" />
</svg>
</button>
</x-slot:content>
<livewire:server.create />
</x-modal-input>
@endif
@endcan
</div>
@if ($servers->count() > 0)
<div class="grid grid-cols-1 gap-4 xl:grid-cols-2">
@@ -127,26 +136,34 @@
@if ($privateKeys->count() === 0)
<div class="flex flex-col gap-1">
<div class='font-bold dark:text-warning'>No private keys found.</div>
<div class="flex items-center gap-1">Before you can add your server, first <x-modal-input
buttonTitle="add" title="New Private Key">
<livewire:security.private-key.create from="server" />
</x-modal-input> a private key
or
go to the <a class="underline dark:text-white" href="{{ route('onboarding') }}" {{ wireNavigate() }}>onboarding</a>
page.
</div>
@can('create', App\Models\Server::class)
<div class="flex items-center gap-1">Before you can add your server, first <x-modal-input
buttonTitle="add" title="New Private Key">
<livewire:security.private-key.create from="server" />
</x-modal-input> a private key
or
go to the <a class="underline dark:text-white"
href="{{ route('onboarding') }}"
{{ wireNavigate() }}>onboarding</a>
page.
</div>
@endcan
</div>
@else
<div class="flex flex-col gap-1">
<div class='font-bold dark:text-warning'>No servers found.</div>
<div class="flex items-center gap-1">
<x-modal-input buttonTitle="Add" title="New Server" :closeOutside="false">
<livewire:server.create />
</x-modal-input> your first server
or
go to the <a class="underline dark:text-white" href="{{ route('onboarding') }}" {{ wireNavigate() }}>onboarding</a>
page.
</div>
@can('create', App\Models\Server::class)
<div class="flex items-center gap-1">
<x-modal-input buttonTitle="Add" title="New Server" :closeOutside="false">
<livewire:server.create />
</x-modal-input> your first server
or
go to the <a class="underline dark:text-white"
href="{{ route('onboarding') }}"
{{ wireNavigate() }}>onboarding</a>
page.
</div>
@endcan
</div>
@endif
@endif
@@ -18,7 +18,7 @@
</form>
</div>
@else
<x-callout type="warning" title="Permission Required">
<x-callout type="danger" title="Insufficient Permissions">
You don't have permission to create new destinations. Please contact your team administrator for access.
</x-callout>
@endcan
@@ -26,9 +26,15 @@
helper="If enabled, a ping (@here) will be sent to the notification when a critical event happens."
label="Ping Enabled" />
</div>
<x-forms.input canGate="update" :canResource="$settings" type="password"
helper="Create a Discord Server and generate a Webhook URL. <br><a class='inline-block underline dark:text-white' href='https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks' target='_blank'>Webhook Documentation</a>"
required id="discordWebhookUrl" label="Webhook" />
@can('update', $settings)
<x-forms.input type="password"
helper="Create a Discord Server and generate a Webhook URL. <br><a class='inline-block underline dark:text-white' href='https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks' target='_blank'>Webhook Documentation</a>"
required id="discordWebhookUrl" label="Webhook" />
@else
<x-forms.input disabled
helper="Create a Discord Server and generate a Webhook URL. <br><a class='inline-block underline dark:text-white' href='https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks' target='_blank'>Webhook Documentation</a>"
required label="Webhook" value="Hidden (only admins can view)" />
@endcan
</form>
<h2 class="mt-4">Notification Settings</h2>
<p class="mb-4">
@@ -81,7 +81,11 @@
</div>
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input canGate="update" :canResource="$settings" id="smtpUsername" label="SMTP Username" />
<x-forms.input canGate="update" :canResource="$settings" id="smtpPassword" type="password" label="SMTP Password" />
@can('update', $settings)
<x-forms.input canGate="update" :canResource="$settings" id="smtpPassword" type="password" label="SMTP Password" />
@else
<x-forms.input disabled label="SMTP Password" value="Hidden (only admins can view)" />
@endcan
<x-forms.input canGate="update" :canResource="$settings" id="smtpTimeout" type="number" helper="Timeout value for sending emails."
label="Timeout" />
</div>
@@ -103,8 +107,12 @@
<div class="flex flex-col">
<div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input canGate="update" :canResource="$settings" required type="password" id="resendApiKey" placeholder="API key"
label="API Key" />
@can('update', $settings)
<x-forms.input canGate="update" :canResource="$settings" required type="password" id="resendApiKey" placeholder="API key"
label="API Key" />
@else
<x-forms.input disabled label="API Key" value="Hidden (only admins can view)" />
@endcan
</div>
</div>
</div>
@@ -24,12 +24,21 @@
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="instantSavePushoverEnabled" id="pushoverEnabled" label="Enabled" />
</div>
<div class="flex gap-2">
<x-forms.input canGate="update" :canResource="$settings" type="password"
helper="Get your User Key in Pushover. You need to be logged in to Pushover to see your user key in the top right corner. <br><a class='inline-block underline dark:text-white' href='https://pushover.net/' target='_blank'>Pushover Dashboard</a>"
required id="pushoverUserKey" label="User Key" />
<x-forms.input canGate="update" :canResource="$settings" type="password"
helper="Generate an API Token/Key in Pushover by creating a new application. <br><a class='inline-block underline dark:text-white' href='https://pushover.net/apps/build' target='_blank'>Create Pushover Application</a>"
required id="pushoverApiToken" label="API Token" />
@can('update', $settings)
<x-forms.input type="password"
helper="Get your User Key in Pushover. You need to be logged in to Pushover to see your user key in the top right corner. <br><a class='inline-block underline dark:text-white' href='https://pushover.net/' target='_blank'>Pushover Dashboard</a>"
required id="pushoverUserKey" label="User Key" />
<x-forms.input type="password"
helper="Generate an API Token/Key in Pushover by creating a new application. <br><a class='inline-block underline dark:text-white' href='https://pushover.net/apps/build' target='_blank'>Create Pushover Application</a>"
required id="pushoverApiToken" label="API Token" />
@else
<x-forms.input disabled
helper="Get your User Key in Pushover. You need to be logged in to Pushover to see your user key in the top right corner. <br><a class='inline-block underline dark:text-white' href='https://pushover.net/' target='_blank'>Pushover Dashboard</a>"
required label="User Key" value="Hidden (only admins can view)" />
<x-forms.input disabled
helper="Generate an API Token/Key in Pushover by creating a new application. <br><a class='inline-block underline dark:text-white' href='https://pushover.net/apps/build' target='_blank'>Create Pushover Application</a>"
required label="API Token" value="Hidden (only admins can view)" />
@endcan
</div>
</form>
<h2 class="mt-4">Notification Settings</h2>
@@ -23,9 +23,15 @@
<div class="w-32">
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="instantSaveSlackEnabled" id="slackEnabled" label="Enabled" />
</div>
<x-forms.input canGate="update" :canResource="$settings" type="password"
helper="Create a Slack APP and generate a Incoming Webhook URL. <br><a class='inline-block underline dark:text-white' href='https://api.slack.com/apps' target='_blank'>Create Slack APP</a>"
required id="slackWebhookUrl" label="Webhook" />
@can('update', $settings)
<x-forms.input type="password"
helper="Create a Slack APP and generate a Incoming Webhook URL. <br><a class='inline-block underline dark:text-white' href='https://api.slack.com/apps' target='_blank'>Create Slack APP</a>"
required id="slackWebhookUrl" label="Webhook" />
@else
<x-forms.input disabled
helper="Create a Slack APP and generate a Incoming Webhook URL. <br><a class='inline-block underline dark:text-white' href='https://api.slack.com/apps' target='_blank'>Create Slack APP</a>"
required label="Webhook" value="Hidden (only admins can view)" />
@endcan
</form>
<h2 class="mt-4">Notification Settings</h2>
<p class="mb-4">
@@ -24,12 +24,21 @@
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="instantSaveTelegramEnabled" id="telegramEnabled" label="Enabled" />
</div>
<div class="flex gap-2">
<x-forms.input canGate="update" :canResource="$settings" type="password" autocomplete="new-password"
helper="Get it from the <a class='inline-block underline dark:text-white' href='https://t.me/botfather' target='_blank'>BotFather Bot</a> on Telegram."
required id="telegramToken" label="Bot API Token" />
<x-forms.input canGate="update" :canResource="$settings" type="password" autocomplete="new-password"
helper="Add your bot to a group chat and add its Chat ID here." required id="telegramChatId"
label="Chat ID" />
@can('update', $settings)
<x-forms.input type="password" autocomplete="new-password"
helper="Get it from the <a class='inline-block underline dark:text-white' href='https://t.me/botfather' target='_blank'>BotFather Bot</a> on Telegram."
required id="telegramToken" label="Bot API Token" />
<x-forms.input type="password" autocomplete="new-password"
helper="Add your bot to a group chat and add its Chat ID here." required id="telegramChatId"
label="Chat ID" />
@else
<x-forms.input disabled autocomplete="new-password"
helper="Get it from the <a class='inline-block underline dark:text-white' href='https://t.me/botfather' target='_blank'>BotFather Bot</a> on Telegram."
required label="Bot API Token" value="Hidden (only admins can view)" />
<x-forms.input disabled autocomplete="new-password"
helper="Add your bot to a group chat and add its Chat ID here." required label="Chat ID"
value="Hidden (only admins can view)" />
@endcan
</div>
</form>
<h2 class="mt-4">Notification Settings</h2>
@@ -28,9 +28,15 @@
</div>
<div class="flex items-end gap-2">
<x-forms.input canGate="update" :canResource="$settings" type="password"
helper="Enter a valid HTTP or HTTPS URL. Coolify will send POST requests to this endpoint when events occur."
required id="webhookUrl" label="Webhook URL (POST)" />
@can('update', $settings)
<x-forms.input type="password"
helper="Enter a valid HTTP or HTTPS URL. Coolify will send POST requests to this endpoint when events occur."
required id="webhookUrl" label="Webhook URL (POST)" />
@else
<x-forms.input disabled
helper="Enter a valid HTTP or HTTPS URL. Coolify will send POST requests to this endpoint when events occur."
required label="Webhook URL (POST)" value="Hidden (only admins can view)" />
@endcan
</div>
</form>
<h2 class="mt-4">Notification Settings</h2>
@@ -338,6 +338,7 @@
class="block w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-coolgray-300">
Download displayed logs
</button>
@can('update', $application)
<button x-on:click="
downloadingAllLogs = true;
$wire.downloadAllLogs().then(logs => {
@@ -368,6 +369,7 @@
Downloading...
</span>
</button>
@endcan
</div>
</div>
</div>
@@ -380,6 +382,7 @@
d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
</button>
@can('update', $application)
<button wire:click="toggleDebug"
title="{{ $is_debug_enabled ? 'Hide Debug Logs' : 'Show Debug Logs' }}"
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 {{ $is_debug_enabled ? '!text-warning' : '' }}">
@@ -389,6 +392,7 @@
d="M12 12.75c1.148 0 2.278.08 3.383.237 1.037.146 1.866.966 1.866 2.013 0 3.728-2.35 6.75-5.25 6.75S6.75 18.728 6.75 15c0-1.046.83-1.867 1.866-2.013A24.204 24.204 0 0 1 12 12.75Zm0 0c2.883 0 5.647.508 8.207 1.44a23.91 23.91 0 0 1-1.152 6.06M12 12.75c-2.883 0-5.647.508-8.208 1.44.125 2.104.52 4.136 1.153 6.06M12 12.75a2.25 2.25 0 0 0 2.248-2.354M12 12.75a2.25 2.25 0 0 1-2.248-2.354M12 8.25c.995 0 1.971-.08 2.922-.236.403-.066.74-.358.795-.762a3.778 3.778 0 0 0-.399-2.25M12 8.25c-.995 0-1.97-.08-2.922-.236-.402-.066-.74-.358-.795-.762a3.734 3.734 0 0 1 .4-2.253M12 8.25a2.25 2.25 0 0 0-2.248 2.146M12 8.25a2.25 2.25 0 0 1 2.248 2.146M8.683 5a6.032 6.032 0 0 1-1.155-1.002c.07-.63.27-1.222.574-1.747m.581 2.749A3.75 3.75 0 0 1 15.318 5m0 0c.427-.283.815-.62 1.155-.999a4.471 4.471 0 0 0-.575-1.752M4.921 6a24.048 24.048 0 0 0-.392 3.314c1.668.546 3.416.914 5.223 1.082M19.08 6c.205 1.08.337 2.187.392 3.314a23.882 23.882 0 0 1-5.223 1.082" />
</svg>
</button>
@endcan
<button title="Follow Logs" :class="alwaysScroll ? '!text-warning' : ''"
x-on:click="toggleScroll"
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
@@ -483,13 +483,14 @@
<div class="flex flex-wrap gap-2">
@if (!str($application->status)->startsWith('exited'))
@if (!$application->destination->server->isSwarm())
<x-modal-confirmation title="Confirm Application Deployment?" buttonTitle="Redeploy"
submitAction="deploy" :actions="[
'This application will be redeployed.',
]" :confirmWithText="false" :confirmWithPassword="false"
step2ButtonText="Confirm">
<x-slot:content>
<x-forms.button title="With rolling update if possible">
<x-forms.button canGate="deploy" :canResource="$application" title="With rolling update if possible">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-orange-400"
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
@@ -512,7 +513,7 @@
]" :confirmWithText="false" :confirmWithPassword="false"
step2ButtonText="Confirm">
<x-slot:content>
<x-forms.button title="Redeploy Swarm Service (rolling update)">
<x-forms.button canGate="deploy" :canResource="$application" title="Redeploy Swarm Service (rolling update)">
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round"
@@ -533,7 +534,7 @@
]" :confirmWithText="false" :confirmWithPassword="false"
step2ButtonText="Confirm">
<x-slot:content>
<x-forms.button title="Restart without rebuilding">
<x-forms.button canGate="deploy" :canResource="$application" title="Restart without rebuilding">
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round"
@@ -547,9 +548,10 @@
</x-forms.button>
</x-slot:content>
</x-modal-confirmation>
@endif
@endif
<x-modal-confirmation title="Confirm Application Stopping?" buttonTitle="Stop"
<x-modal-confirmation :disabled="!auth()->user()->can('deploy', $application)" :authDisabled="!auth()->user()->can('deploy', $application)" title="Confirm Application Stopping?" buttonTitle="Stop"
submitAction="stop" :checkboxes="$checkboxes" :actions="[
'This application will be stopped.',
'All non-persistent data of this application will be deleted.',
@@ -571,13 +573,14 @@
</x-slot:button-title>
</x-modal-confirmation>
@else
<x-modal-confirmation title="Confirm Application Deployment?" buttonTitle="Deploy"
submitAction="deploy" :actions="[
'This application will be deployed.',
]" :confirmWithText="false" :confirmWithPassword="false"
step2ButtonText="Confirm">
<x-slot:content>
<x-forms.button>
<x-forms.button canGate="deploy" :canResource="$application">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
@@ -588,6 +591,7 @@
</x-forms.button>
</x-slot:content>
</x-modal-confirmation>
@endif
</div>
</div>
@@ -1,32 +1,40 @@
<form wire:submit="submit">
<div class="flex gap-2 pb-2">
<div class="flex flex-col gap-3 pb-4 sm:flex-row sm:items-center">
<h2>Scheduled Backup</h2>
<x-forms.button type="submit">
Save
</x-forms.button>
@if (str($status)->startsWith('running'))
<livewire:project.database.backup-now :backup="$backup" />
@endif
@if ($backup->database_id !== 0)
<x-modal-confirmation title="Confirm Backup Schedule Deletion?" buttonTitle="Delete Backups and Schedule"
isErrorButton submitAction="delete" :checkboxes="$checkboxes" :actions="[
'The selected backup schedule will be deleted.',
'Scheduled backups for this database will be stopped (if this is the only backup schedule for this database).',
]"
confirmationText="{{ $backup->database->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Database Name of the scheduled backups below"
shortConfirmationLabel="Database Name" />
@endif
<div class="flex flex-col gap-2 sm:flex-row sm:flex-wrap">
<x-forms.button type="submit" class="w-full sm:w-auto">
Save
</x-forms.button>
@if (str($status)->startsWith('running'))
<x-forms.button wire:click='backupNow' class="w-full sm:w-auto">Backup Now</x-forms.button>
@endif
@if ($backup->database_id !== 0)
<div class="w-full sm:w-auto">
<x-modal-confirmation title="Confirm Backup Schedule Deletion?" isErrorButton submitAction="delete"
:checkboxes="$checkboxes" :actions="[
'The selected backup schedule will be deleted.',
'Scheduled backups for this database will be stopped (if this is the only backup schedule for this database).',
]"
confirmationText="{{ $backup->database->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Database Name of the scheduled backups below"
shortConfirmationLabel="Database Name">
<x-slot:trigger>
<x-forms.button isError class="w-full sm:w-auto">Delete Backups and Schedule</x-forms.button>
</x-slot:trigger>
</x-modal-confirmation>
</div>
@endif
</div>
</div>
<div class="w-64 pb-2">
<div class="w-full max-w-md pb-2">
<x-forms.checkbox instantSave label="Backup Enabled" id="backupEnabled" />
@if ($s3s->count() > 0)
@if ($availableS3Storages->count() > 0)
<x-forms.checkbox instantSave label="S3 Enabled" id="saveS3" />
@else
<x-forms.checkbox instantSave helper="No validated S3 storage available." label="S3 Enabled" id="saveS3"
disabled />
@endif
@if ($backup->save_s3)
@if ($saveS3)
<x-forms.checkbox instantSave label="Disable Local Backup" id="disableLocalBackup"
helper="When enabled, backup files will be deleted from local storage immediately after uploading to S3. This requires S3 backup to be enabled." />
@else
@@ -34,16 +42,27 @@
helper="When enabled, backup files will be deleted from local storage immediately after uploading to S3. This requires S3 backup to be enabled." />
@endif
</div>
@if ($backup->save_s3)
<div class="pb-6">
<x-forms.select id="s3StorageId" label="S3 Storage" required>
<option value="default" disabled>Select a S3 storage</option>
@foreach ($s3s as $s3)
<div class="w-full max-w-md pb-6">
<div class="flex gap-1 items-center mb-1 text-sm font-medium">
<span>S3 Storage</span>
@if (!$saveS3)
<span class="text-xs font-normal text-warning">(currently disabled)</span>
@endif
@if ($saveS3)
<x-highlighted text="*" />
@endif
</div>
<x-forms.select id="s3StorageId" wire:model.live="s3StorageId" :required="$saveS3"
:disabled="$availableS3Storages->isEmpty()">
@if ($availableS3Storages->isEmpty())
<option value="">No S3 storage available</option>
@else
@foreach ($availableS3Storages as $s3)
<option value="{{ $s3->id }}">{{ $s3->name }}</option>
@endforeach
</x-forms.select>
</div>
@endif
@endif
</x-forms.select>
</div>
<div class="flex flex-col gap-2">
<h3>Settings</h3>
<div class="flex gap-2 flex-col ">
@@ -80,7 +99,7 @@
@endif
@endif
</div>
<div class="flex gap-2">
<div class="grid grid-cols-1 gap-2 md:grid-cols-3">
<x-forms.input label="Frequency" id="frequency" required />
<x-forms.input label="Timezone" id="timezone" disabled
helper="The timezone of the server where the backup is scheduled to run (if not set, the instance timezone will be used)" required />
@@ -98,7 +117,7 @@
<div class="flex gap-6 flex-col">
<div>
<h4 class="mb-3 font-medium">Local Backup Retention</h4>
<div class="flex gap-2">
<div class="grid grid-cols-1 gap-2 md:grid-cols-3">
<x-forms.input label="Number of backups to keep" id="databaseBackupRetentionAmountLocally"
type="number" min="0"
helper="Keeps only the specified number of most recent backups on the server. Set to 0 for unlimited backups." required />
@@ -111,10 +130,10 @@
</div>
</div>
@if ($backup->save_s3)
@if ($saveS3)
<div>
<h4 class="mb-3 font-medium">S3 Storage Retention</h4>
<div class="flex gap-2">
<div class="grid grid-cols-1 gap-2 md:grid-cols-3">
<x-forms.input label="Number of backups to keep" id="databaseBackupRetentionAmountS3"
type="number" min="0"
helper="Keeps only the specified number of most recent backups on S3 storage. Set to 0 for unlimited backups." required />
@@ -1,7 +1,7 @@
<div wire:init='refreshBackupExecutions'>
@isset($backup)
<div class="flex items-center gap-2">
<h3 class="py-4">Executions <span class="text-xs">({{ $executions_count }})</span></h3>
<div class="flex flex-col gap-3 py-4 sm:flex-row sm:flex-wrap sm:items-center">
<h3 class="py-0">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 }}')">
@@ -21,13 +21,19 @@
</x-forms.button>
</div>
@endif
<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 class="flex flex-col gap-2 sm:flex-row sm:flex-wrap">
<x-forms.button wire:click='cleanupFailed' class="w-full sm:w-auto">Cleanup Failed Backups</x-forms.button>
<x-modal-confirmation title="Cleanup Deleted Backup Entries?" 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">
<x-slot:trigger>
<x-forms.button isError class="w-full sm:w-auto">Cleanup Deleted</x-forms.button>
</x-slot:trigger>
</x-modal-confirmation>
</div>
</div>
<div @if (!$skip) wire:poll.5000ms="refreshBackupExecutions" @endif
class="flex flex-col gap-4">
@@ -87,7 +93,7 @@
<div class="text-gray-600 dark:text-gray-400 text-sm">
Location: {{ data_get($execution, 'filename', 'N/A') }}
</div>
<div class="flex items-center gap-3 mt-2">
<div class="flex flex-col gap-2 mt-2 sm:flex-row sm:flex-wrap sm:items-center sm:gap-3">
<div class="text-gray-600 dark:text-gray-400 text-sm">
Backup Availability:
</div>
@@ -154,9 +160,9 @@
<pre class="whitespace-pre-wrap text-sm">{{ data_get($execution, 'message') }}</pre>
</div>
@endif
<div class="flex gap-2 mt-4">
<div class="grid grid-cols-2 gap-2 mt-4 sm:flex sm:flex-wrap">
@if (data_get($execution, 'status') === 'success')
<x-forms.button class="dark:hover:bg-coolgray-400"
<x-forms.button class="w-full dark:hover:bg-coolgray-400 sm:w-auto"
x-on:click="download_file('{{ data_get($execution, 'id') }}')">Download</x-forms.button>
@endif
@php
@@ -175,11 +181,15 @@
$deleteActions[] = 'This backup execution record will be deleted.';
}
@endphp
<x-modal-confirmation title="Confirm Backup Deletion?" buttonTitle="Delete" isErrorButton
<x-modal-confirmation title="Confirm Backup Deletion?" isErrorButton
submitAction="deleteBackup({{ data_get($execution, 'id') }})" :checkboxes="$executionCheckboxes"
:actions="$deleteActions" confirmationText="{{ data_get($execution, 'filename') }}"
confirmationLabel="Please confirm the execution of the actions by entering the Backup Filename below"
shortConfirmationLabel="Backup Filename" 1 />
shortConfirmationLabel="Backup Filename">
<x-slot:trigger>
<x-forms.button isError class="w-full sm:w-auto">Delete</x-forms.button>
</x-slot:trigger>
</x-modal-confirmation>
</div>
</div>
@empty
@@ -6,7 +6,7 @@
<livewire:project.shared.configuration-checker :resource="$database" />
<livewire:project.database.heading :database="$database" />
<div>
<livewire:project.database.backup-edit :backup="$backup" :s3s="$s3s" :status="data_get($database, 'status')" />
<livewire:project.database.backup-edit :backup="$backup" :available-s3-storages="$s3s" :status="data_get($database, 'status')" />
<livewire:project.database.backup-executions :backup="$backup" />
</div>
</div>
@@ -20,8 +20,12 @@
<div class="flex gap-2">
<x-forms.input label="Initial Username" id="clickhouseAdminUser" placeholder="If empty: clickhouse"
readonly helper="You can only change this in the database." canGate="update" :canResource="$database" />
<x-forms.input label="Initial Password" id="clickhouseAdminPassword" type="password" required readonly
helper="You can only change this in the database." canGate="update" :canResource="$database" />
@if ($isPasswordHiddenForMember)
<x-forms.input label="Initial Password" disabled value="Hidden (only admins can view)" />
@else
<x-forms.input label="Initial Password" id="clickhouseAdminPassword" type="password" required readonly
helper="You can only change this in the database." canGate="update" :canResource="$database" />
@endif
</div>
@else
<div class=" dark:text-warning">Please verify these values. You can only modify them before the initial
@@ -29,8 +33,12 @@
</div>
<div class="flex gap-2">
<x-forms.input label="Username" id="clickhouseAdminUser" required canGate="update" :canResource="$database" />
<x-forms.input label="Password" id="clickhouseAdminPassword" type="password" required canGate="update"
:canResource="$database" />
@if ($isPasswordHiddenForMember)
<x-forms.input label="Password" disabled value="Hidden (only admins can view)" />
@else
<x-forms.input label="Password" id="clickhouseAdminPassword" type="password" required canGate="update"
:canResource="$database" />
@endif
</div>
@endif
<x-forms.input
@@ -21,16 +21,24 @@
@if ($database->started_at)
<div class="flex gap-2">
<x-forms.input label="Initial Password" id="dragonflyPassword" type="password" required readonly
helper="You can only change this in the database." canGate="update" :canResource="$database" />
@if ($isPasswordHiddenForMember)
<x-forms.input label="Initial Password" disabled value="Hidden (only admins can view)" />
@else
<x-forms.input label="Initial Password" id="dragonflyPassword" type="password" required readonly
helper="You can only change this in the database." canGate="update" :canResource="$database" />
@endif
</div>
@else
<div class=" dark:text-warning">Please verify these values. You can only modify them before the initial
start. After that, you need to modify it in the database.
</div>
<div class="flex gap-2">
<x-forms.input label="Password" id="dragonflyPassword" type="password" required canGate="update"
:canResource="$database" />
@if ($isPasswordHiddenForMember)
<x-forms.input label="Password" disabled value="Hidden (only admins can view)" />
@else
<x-forms.input label="Password" id="dragonflyPassword" type="password" required canGate="update"
:canResource="$database" />
@endif
</div>
@endif
<div class="flex flex-col gap-2">
@@ -216,7 +216,8 @@
<div class="flex flex-wrap gap-2 items-center">
<div class="hidden flex-wrap items-center gap-2 md:flex">
@if (!str($database->status)->startsWith('exited'))
<x-forms.button title="Restart" @click="document.getElementById('database-restart-trigger')?.click()">
<x-forms.button canGate="manage" :canResource="$database" title="Restart" @click="document.getElementById('database-restart-trigger')?.click()">
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
@@ -227,7 +228,7 @@
</svg>
Restart
</x-forms.button>
<x-forms.button isError title="Stop" @click="document.getElementById('database-stop-trigger')?.click()">
<x-forms.button canGate="manage" :canResource="$database" isError title="Stop" @click="document.getElementById('database-stop-trigger')?.click()">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
@@ -241,7 +242,8 @@
Stop
</x-forms.button>
@else
<button @click="document.getElementById('database-start-trigger')?.click()" class="gap-2 button">
<x-forms.button canGate="manage" :canResource="$database" @click="document.getElementById('database-start-trigger')?.click()" class="gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
@@ -249,7 +251,7 @@
<path d="M7 4v16l13 -8z" />
</svg>
Start
</button>
</x-forms.button>
@endif
</div>
@script
@@ -18,16 +18,24 @@
@if ($database->started_at)
<div class="flex gap-2">
<x-forms.input label="Initial Password" id="keydbPassword" type="password" required readonly
helper="You can only change this in the database." canGate="update" :canResource="$database" />
@if ($isPasswordHiddenForMember)
<x-forms.input label="Initial Password" disabled value="Hidden (only admins can view)" />
@else
<x-forms.input label="Initial Password" id="keydbPassword" type="password" required readonly
helper="You can only change this in the database." canGate="update" :canResource="$database" />
@endif
</div>
@else
<div class=" dark:text-warning">Please verify these values. You can only modify them before the initial
start. After that, you need to modify it in the database.
</div>
<div class="flex gap-2">
<x-forms.input label="Password" id="keydbPassword" type="password" required canGate="update"
:canResource="$database" />
@if ($isPasswordHiddenForMember)
<x-forms.input label="Password" disabled value="Hidden (only admins can view)" />
@else
<x-forms.input label="Password" id="keydbPassword" type="password" required canGate="update"
:canResource="$database" />
@endif
</div>
@endif
<x-forms.input
@@ -20,15 +20,23 @@
</div>
@if ($database->started_at)
<div class="flex xl:flex-row flex-col gap-2">
<x-forms.input label="Root Password" id="mariadbRootPassword" type="password" required
helper="If you change this in the database, please sync it here, otherwise automations (like backups) won't work."
canGate="update" :canResource="$database" />
@if ($isPasswordHiddenForMember)
<x-forms.input label="Root Password" disabled value="Hidden (only admins can view)" />
@else
<x-forms.input label="Root Password" id="mariadbRootPassword" type="password" required
helper="If you change this in the database, please sync it here, otherwise automations (like backups) won't work."
canGate="update" :canResource="$database" />
@endif
<x-forms.input label="Normal User" id="mariadbUser" required
helper="If you change this in the database, please sync it here, otherwise automations (like backups) won't work."
canGate="update" :canResource="$database" />
<x-forms.input label="Normal User Password" id="mariadbPassword" type="password" required
helper="If you change this in the database, please sync it here, otherwise automations (like backups) won't work."
canGate="update" :canResource="$database" />
@if ($isPasswordHiddenForMember)
<x-forms.input label="Normal User Password" disabled value="Hidden (only admins can view)" />
@else
<x-forms.input label="Normal User Password" id="mariadbPassword" type="password" required
helper="If you change this in the database, please sync it here, otherwise automations (like backups) won't work."
canGate="update" :canResource="$database" />
@endif
</div>
<div class="flex flex-col gap-2">
<x-forms.input label="Initial Database" id="mariadbDatabase"
@@ -37,12 +45,20 @@
</div>
@else
<div class="flex xl:flex-row flex-col gap-2 pb-2">
<x-forms.input label="Root Password" id="mariadbRootPassword" type="password"
helper="You can only change this in the database." canGate="update" :canResource="$database" />
@if ($isPasswordHiddenForMember)
<x-forms.input label="Root Password" disabled value="Hidden (only admins can view)" />
@else
<x-forms.input label="Root Password" id="mariadbRootPassword" type="password"
helper="You can only change this in the database." canGate="update" :canResource="$database" />
@endif
<x-forms.input label="Normal User" id="mariadbUser" required
helper="You can only change this in the database." canGate="update" :canResource="$database" />
<x-forms.input label="Normal User Password" id="mariadbPassword" type="password" required
helper="You can only change this in the database." canGate="update" :canResource="$database" />
@if ($isPasswordHiddenForMember)
<x-forms.input label="Normal User Password" disabled value="Hidden (only admins can view)" />
@else
<x-forms.input label="Normal User Password" id="mariadbPassword" type="password" required
helper="You can only change this in the database." canGate="update" :canResource="$database" />
@endif
</div>
<div class="flex flex-col gap-2">
<x-forms.input label="Initial Database" id="mariadbDatabase"
@@ -24,10 +24,14 @@
placeholder="If empty: postgres"
helper="If you change this in the database, please sync it here, otherwise automations (like backups) won't work."
canGate="update" :canResource="$database" />
<x-forms.input label="Initial Password" id="mongoInitdbRootPassword" type="password"
required
helper="If you change this in the database, please sync it here, otherwise automations (like backups) won't work."
canGate="update" :canResource="$database" />
@if ($isPasswordHiddenForMember)
<x-forms.input label="Initial Password" disabled value="Hidden (only admins can view)" />
@else
<x-forms.input label="Initial Password" id="mongoInitdbRootPassword" type="password"
required
helper="If you change this in the database, please sync it here, otherwise automations (like backups) won't work."
canGate="update" :canResource="$database" />
@endif
<x-forms.input label="Initial Database" id="mongoInitdbDatabase"
placeholder="If empty, it will be the same as Username." readonly
helper="You can only change this in the database." canGate="update" :canResource="$database" />
@@ -36,8 +40,12 @@
<div class="flex xl:flex-row flex-col gap-2 pb-2">
<x-forms.input required label="Username" id="mongoInitdbRootUsername"
placeholder="If empty: postgres" canGate="update" :canResource="$database" />
<x-forms.input label="Password" id="mongoInitdbRootPassword" type="password" required
canGate="update" :canResource="$database" />
@if ($isPasswordHiddenForMember)
<x-forms.input label="Password" disabled value="Hidden (only admins can view)" />
@else
<x-forms.input label="Password" id="mongoInitdbRootPassword" type="password" required
canGate="update" :canResource="$database" />
@endif
<x-forms.input required label="Database" id="mongoInitdbDatabase"
placeholder="If empty, it will be the same as Username." canGate="update" :canResource="$database" />
</div>
@@ -20,12 +20,20 @@
</div>
@if ($database->started_at)
<div class="flex xl:flex-row flex-col gap-2">
<x-forms.input label="Root Password" id="mysqlRootPassword" type="password" required
helper="If you change this in the database, please sync it here, otherwise automations (like backups) won't work." canGate="update" :canResource="$database" />
@if ($isPasswordHiddenForMember)
<x-forms.input label="Root Password" disabled value="Hidden (only admins can view)" />
@else
<x-forms.input label="Root Password" id="mysqlRootPassword" type="password" required
helper="If you change this in the database, please sync it here, otherwise automations (like backups) won't work." canGate="update" :canResource="$database" />
@endif
<x-forms.input label="Normal User" id="mysqlUser" required
helper="If you change this in the database, please sync it here, otherwise automations (like backups) won't work." canGate="update" :canResource="$database" />
<x-forms.input label="Normal User Password" id="mysqlPassword" type="password" required
helper="If you change this in the database, please sync it here, otherwise automations (like backups) won't work." canGate="update" :canResource="$database" />
@if ($isPasswordHiddenForMember)
<x-forms.input label="Normal User Password" disabled value="Hidden (only admins can view)" />
@else
<x-forms.input label="Normal User Password" id="mysqlPassword" type="password" required
helper="If you change this in the database, please sync it here, otherwise automations (like backups) won't work." canGate="update" :canResource="$database" />
@endif
</div>
<div class="flex flex-col gap-2">
<x-forms.input label="Initial Database" id="mysqlDatabase"
@@ -34,12 +42,20 @@
</div>
@else
<div class="flex xl:flex-row flex-col gap-4 pb-2">
<x-forms.input label="Root Password" id="mysqlRootPassword" type="password"
helper="You can only change this in the database." canGate="update" :canResource="$database" />
@if ($isPasswordHiddenForMember)
<x-forms.input label="Root Password" disabled value="Hidden (only admins can view)" />
@else
<x-forms.input label="Root Password" id="mysqlRootPassword" type="password"
helper="You can only change this in the database." canGate="update" :canResource="$database" />
@endif
<x-forms.input label="Normal User" id="mysqlUser" required
helper="You can only change this in the database." canGate="update" :canResource="$database" />
<x-forms.input label="Normal User Password" id="mysqlPassword" type="password" required
helper="You can only change this in the database." canGate="update" :canResource="$database" />
@if ($isPasswordHiddenForMember)
<x-forms.input label="Normal User Password" disabled value="Hidden (only admins can view)" />
@else
<x-forms.input label="Normal User Password" id="mysqlPassword" type="password" required
helper="You can only change this in the database." canGate="update" :canResource="$database" />
@endif
</div>
<div class="flex flex-col gap-2">
<x-forms.input label="Initial Database" id="mysqlDatabase"
@@ -37,9 +37,13 @@
<x-forms.input label="Username" id="postgresUser" placeholder="If empty: postgres" canGate="update"
:canResource="$database"
helper="If you change this in the database, please sync it here, otherwise automations (like backups) won't work." />
<x-forms.input label="Password" id="postgresPassword" type="password" required canGate="update"
:canResource="$database"
helper="If you change this in the database, please sync it here, otherwise automations (like backups) won't work." />
@if ($isPasswordHiddenForMember)
<x-forms.input label="Password" disabled value="Hidden (only admins can view)" />
@else
<x-forms.input label="Password" id="postgresPassword" type="password" required canGate="update"
:canResource="$database"
helper="If you change this in the database, please sync it here, otherwise automations (like backups) won't work." />
@endif
<x-forms.input label="Initial Database" id="postgresDb"
placeholder="If empty, it will be the same as Username." readonly
helper="You can only change this in the database." />
@@ -48,8 +52,12 @@
<div class="flex xl:flex-row flex-col gap-2 pb-2">
<x-forms.input label="Username" id="postgresUser" placeholder="If empty: postgres" canGate="update"
:canResource="$database" />
<x-forms.input label="Password" id="postgresPassword" type="password" required canGate="update"
:canResource="$database" />
@if ($isPasswordHiddenForMember)
<x-forms.input label="Password" disabled value="Hidden (only admins can view)" />
@else
<x-forms.input label="Password" id="postgresPassword" type="password" required canGate="update"
:canResource="$database" />
@endif
<x-forms.input label="Initial Database" id="postgresDb"
placeholder="If empty, it will be the same as Username." canGate="update" :canResource="$database" />
</div>
@@ -134,7 +142,8 @@
</div>
<div class="flex flex-col gap-2">
@forelse($initScripts ?? [] as $script)
<livewire:project.database.init-script :script="$script" :wire:key="$script['index']" />
<livewire:project.database.init-script :database="$database" :script="$script"
:wire:key="$script['index']" />
@empty
<div>No initialization scripts found.</div>
@endforelse
@@ -26,8 +26,12 @@
<x-forms.input label="Username" id="redisUsername"
helper="You can only change this in the database." canGate="update" :canResource="$database" />
@endif
<x-forms.input label="Password" id="redisPassword" type="password"
helper="You can only change this in the database." canGate="update" :canResource="$database" />
@if ($isPasswordHiddenForMember)
<x-forms.input label="Password" disabled value="Hidden (only admins can view)" />
@else
<x-forms.input label="Password" id="redisPassword" type="password"
helper="You can only change this in the database." canGate="update" :canResource="$database" />
@endif
</div>
@else
<div class="pt-2 dark:text-warning">You can only change the username and password in the database after
@@ -42,13 +46,17 @@
Note: If the environment variable REDIS_USERNAME is set as a shared variable (environment, project, or team-based), this input field will become read-only."
:disabled="$this->isSharedVariable('REDIS_USERNAME')" canGate="update" :canResource="$database" />
@endif
<x-forms.input label="Password" id="redisPassword" type="password" required
helper="You can change the Redis Password in the input field below or by editing the value of the REDIS_PASSWORD environment variable.
@if ($isPasswordHiddenForMember)
<x-forms.input label="Password" disabled value="Hidden (only admins can view)" />
@else
<x-forms.input label="Password" id="redisPassword" type="password" required
helper="You can change the Redis Password in the input field below or by editing the value of the REDIS_PASSWORD environment variable.
<br><br>
If you change the Redis Password in the database, please sync it here, otherwise automations (like backups) won't work.
<br><br>
Note: If the environment variable REDIS_PASSWORD is set as a shared variable (environment, project, or team-based), this input field will become read-only."
:disabled="$this->isSharedVariable('REDIS_PASSWORD')" canGate="update" :canResource="$database" />
:disabled="$this->isSharedVariable('REDIS_PASSWORD')" canGate="update" :canResource="$database" />
@endif
</div>
@endif
</div>
@@ -216,7 +216,7 @@
@if ($type === 'service-database' && $selectedBackup)
<div class="pt-10">
<livewire:project.database.backup-edit wire:key="{{ $selectedBackup->id }}" :backup="$selectedBackup"
:s3s="$s3s" :status="data_get($database, 'status')" />
:available-s3-storages="$s3s" :status="data_get($database, 'status')" />
<livewire:project.database.backup-executions wire:key="{{ $selectedBackup->uuid }}" :backup="$selectedBackup"
:database="$database" />
</div>
@@ -2,5 +2,6 @@
<x-database-status-info :database="$database" :label="$label" :db-url="$dbUrl" :db-url-public="$dbUrlPublic"
:enable-ssl="$enableSsl" :ssl-mode="$sslMode" :certificate-valid-until="$certificateValidUntil"
:supports-ssl="$supportsSsl" :ssl-mode-options="$sslModeOptions" :ssl-mode-helper="$sslModeHelper"
:show-public-url-placeholder="$showPublicUrlPlaceholder" :is-exited="$isExited" />
:show-public-url-placeholder="$showPublicUrlPlaceholder" :is-exited="$isExited"
:is-password-hidden-for-member="$isPasswordHiddenForMember" />
</div>
@@ -154,7 +154,7 @@
<div class="grid justify-start grid-cols-1 gap-4 text-left xl:grid-cols-3">
<template x-for="service in filteredServices" :key="service.name">
<div class="relative" x-on:click="setType('one-click-service-' + service.name)"
<div class="relative" x-on:click="setType('one-click-service-' + service.id)"
:class="{ 'cursor-pointer': !selecting, 'cursor-not-allowed opacity-50': selecting }">
<x-resource-view>
<x-slot:title>
@@ -214,7 +214,7 @@
</div>
</template>
<template x-if="shouldShowDocIcon(service)">
<a :href="getDocLink(service) || coolifyDocsUrl(service.name)" target="_blank"
<a :href="getDocLink(service) || coolifyDocsUrl(service)" target="_blank"
@click.stop @mouseenter="resolveDocLink(service)"
class="absolute top-2 right-2 p-1.5 rounded hover:bg-neutral-200 dark:hover:bg-coolgray-300 transition-colors"
:class="{ 'opacity-50': docCheckInProgress[service.name] }"
@@ -287,8 +287,8 @@
// Remove flavor suffixes: -with-*, -without-*
return normalized.replace(/-(with|without)-.+$/, '');
},
coolifyDocsUrl(serviceName) {
const baseName = this.extractBaseServiceName(serviceName);
coolifyDocsUrl(service) {
const baseName = service.docsSlug || this.extractBaseServiceName(service.name);
return 'https://coolify.io/docs/services/' + baseName;
},
officialDocsUrl(service) {
@@ -322,7 +322,7 @@
this.docCheckInProgress[serviceName] = true;
// 1. Try Coolify docs first
const coolifyUrl = this.coolifyDocsUrl(serviceName);
const coolifyUrl = this.coolifyDocsUrl(service);
const coolifyExists = await this.checkUrlExists(coolifyUrl);
if (coolifyExists) {
@@ -4,6 +4,10 @@
<div class="w-full p-2 text-sm rounded bg-warning/10 text-warning">
File on server exceeds 5 MB and cannot be edited from the UI. Edit it directly on the server.
</div>
@elseif ($fileStorage->is_host_file)
<div class="w-full p-2 text-sm rounded bg-warning/10 text-warning">
This host file mount is bind-only. Coolify will not create, edit, load, chmod, or delete the source file.
</div>
@elseif ($isReadOnly)
<div class="w-full p-2 text-sm rounded bg-warning/10 text-warning">
@if ($fileStorage->is_directory)
@@ -32,7 +36,14 @@
@if (!$isReadOnly)
@can('update', $resource)
<div class="flex gap-2">
@if ($fileStorage->is_directory)
@if ($fileStorage->is_host_file)
<x-modal-confirmation :ignoreWire="false" title="Confirm Host File Mount Removal?"
buttonTitle="Delete" isErrorButton submitAction="delete" :checkboxes="$hostFileDeletionCheckboxes"
:actions="['Only the mount configuration will be removed. The host file will not be deleted.']"
confirmationText="{{ $fs_path }}"
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
shortConfirmationLabel="Filepath" />
@elseif ($fileStorage->is_directory)
<x-modal-confirmation :ignoreWire="false" title="Confirm Directory Conversion to File?"
buttonTitle="Convert to file" submitAction="convertToFile" :actions="[
'All files in this directory will be permanently deleted and an empty file will be created in its place.',
@@ -68,7 +79,7 @@
@endif
</div>
@endcan
@if (!$fileStorage->is_directory)
@if (!$fileStorage->is_directory && !$fileStorage->is_host_file)
@can('update', $resource)
@if (data_get($resource, 'settings.is_preserve_repository_enabled'))
<div class="w-full sm:w-96">
@@ -99,8 +110,8 @@
@endif
@else
{{-- Read-only view --}}
@if (!$fileStorage->is_directory)
@can('view', $resource)
@if (!$fileStorage->is_directory && !$fileStorage->is_host_file)
@can('update', $resource)
<div class="flex gap-2">
<x-forms.button type="button" wire:click="loadStorageOnServer">Load from
server</x-forms.button>
@@ -369,7 +369,9 @@
<div class="flex flex-wrap items-center gap-2">
<x-services.advanced :service="$service" />
@if (str($service->status)->contains('running'))
<x-forms.button title="Restart" @click="document.getElementById('service-restart-trigger')?.click()">
<x-forms.button canGate="deploy" :canResource="$service" title="Restart" @click="document.getElementById('service-restart-trigger')?.click()">
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
@@ -379,7 +381,8 @@
</svg>
Restart
</x-forms.button>
<x-forms.button isError title="Stop" @click="document.getElementById('service-stop-trigger')?.click()">
<x-forms.button canGate="stop" :canResource="$service" isError title="Stop" @click="document.getElementById('service-stop-trigger')?.click()">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
@@ -393,7 +396,8 @@
Stop
</x-forms.button>
@elseif (str($service->status)->contains('degraded'))
<x-forms.button title="Restart" @click="document.getElementById('service-restart-trigger')?.click()">
<x-forms.button canGate="deploy" :canResource="$service" title="Restart" @click="document.getElementById('service-restart-trigger')?.click()">
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
@@ -403,7 +407,8 @@
</svg>
Restart
</x-forms.button>
<x-forms.button isError title="Stop" @click="document.getElementById('service-stop-trigger')?.click()">
<x-forms.button canGate="stop" :canResource="$service" isError title="Stop" @click="document.getElementById('service-stop-trigger')?.click()">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
@@ -417,7 +422,8 @@
Stop
</x-forms.button>
@elseif (str($service->status)->contains('exited'))
<button @click="document.getElementById('service-start-trigger')?.click()" class="gap-2 button">
<x-forms.button canGate="deploy" :canResource="$service" @click="document.getElementById('service-start-trigger')?.click()" class="gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
@@ -425,9 +431,10 @@
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</button>
</x-forms.button>
@else
<x-forms.button isError title="Stop" @click="document.getElementById('service-stop-trigger')?.click()">
<x-forms.button canGate="stop" :canResource="$service" isError title="Stop" @click="document.getElementById('service-stop-trigger')?.click()">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
@@ -440,7 +447,8 @@
</svg>
Stop
</x-forms.button>
<button @click="document.getElementById('service-start-trigger')?.click()" class="gap-2 button">
<x-forms.button canGate="deploy" :canResource="$service" @click="document.getElementById('service-start-trigger')?.click()" class="gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
@@ -448,7 +456,7 @@
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</button>
</x-forms.button>
@endif
</div>
</div>
@@ -522,11 +530,9 @@
);
return;
}
window.dispatchEvent(new CustomEvent('startservice'));
$wire.$call('start');
});
$wire.$on('forceDeployEvent', () => {
window.dispatchEvent(new CustomEvent('startservice'));
$wire.$call('forceDeploy');
});
$wire.$on('restartEvent', async () => {
@@ -539,12 +545,10 @@
}
$wire.$dispatch('info',
'Gracefully stopping service.<br/><br/>It could take a while depending on the service.');
window.dispatchEvent(new CustomEvent('startservice'));
$wire.$call('restart');
});
$wire.$on('pullAndRestartEvent', () => {
$wire.$dispatch('info', 'Pulling new images and restarting service.');
window.dispatchEvent(new CustomEvent('startservice'));
$wire.$call('pullAndRestartEvent');
});
$wire.$on('cleanupEvent', () => {
@@ -45,10 +45,14 @@
<x-helper helper="Variable name: {{ $serviceName }}" />
@endif
</div>
<x-forms.input canGate="update" :canResource="$service"
type="{{ data_get($field, 'isPassword') ? 'password' : 'text' }}"
required="{{ str(data_get($field, 'rules'))?->contains('required') }}"
id="fields.{{ $serviceName }}.value"></x-forms.input>
@if ($isPasswordHiddenForMember && data_get($field, 'isPassword'))
<x-forms.input disabled value="Hidden (only admins can view)" />
@else
<x-forms.input canGate="update" :canResource="$service"
type="{{ data_get($field, 'isPassword') ? 'password' : 'text' }}"
required="{{ str(data_get($field, 'rules'))?->contains('required') }}"
id="fields.{{ $serviceName }}.value"></x-forms.input>
@endif
@endforeach
</div>
@endif
@@ -22,11 +22,13 @@
dropdownOpen: false,
volumeModalOpen: false,
fileModalOpen: false,
hostFileModalOpen: false,
directoryModalOpen: false
}"
@close-storage-modal.window="
if ($event.detail === 'volume') volumeModalOpen = false;
if ($event.detail === 'file') fileModalOpen = false;
if ($event.detail === 'host-file') hostFileModalOpen = false;
if ($event.detail === 'directory') directoryModalOpen = false;
">
<div class="relative" @click.outside="dropdownOpen = false">
@@ -62,6 +64,15 @@
</svg>
File Mount
</a>
<a class="dropdown-item"
@click="hostFileModalOpen = true; dropdownOpen = false">
<svg class="size-4" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
Host File Mount
</a>
<a class="dropdown-item"
@click="directoryModalOpen = true; dropdownOpen = false">
<svg class="size-4" fill="none" stroke="currentColor"
@@ -188,14 +199,28 @@
}
})">
<form class="flex flex-col w-full gap-2 rounded-sm"
x-data="{
hostPath: @js($this->fileStorageHostPath()),
filePath: @entangle('file_storage_path'),
previewPath() {
const path = (this.filePath || '').trim();
return this.hostPath + (path === '' ? '/' : (path.startsWith('/') ? path : `/${path}`));
},
}"
wire:submit='submitFileStorage'>
<div class="flex flex-col">
<div>Actual file mounted from the host system to the container.</div>
<div>This file will be created on the host, then mounted into the container.</div>
</div>
<div class="flex flex-col gap-2">
<div class="p-2 text-xs rounded-sm bg-neutral-100 dark:bg-coolgray-200">
<div class="mb-1 font-medium">Host file path</div>
<code class="break-all" x-text="previewPath()">{{ $this->fileStoragePreviewPath() }}</code>
</div>
<x-forms.input canGate="update" :canResource="$resource"
placeholder="/etc/nginx/nginx.conf" id="file_storage_path"
label="Destination Path" required
x-on:input="filePath = $event.target.value"
helper="File location inside the container" />
<x-forms.textarea canGate="update" :canResource="$resource" label="Content"
id="file_storage_content"></x-forms.textarea>
@@ -209,6 +234,67 @@
</div>
</template>
{{-- Host File Modal --}}
<template x-teleport="body">
<div x-show="hostFileModalOpen" @keydown.window.escape="hostFileModalOpen=false"
class="fixed top-0 left-0 lg:px-0 px-4 z-99 flex items-center justify-center w-screen h-screen">
<div x-show="hostFileModalOpen" x-transition:enter="ease-out duration-100"
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-100" x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0" @click="hostFileModalOpen=false"
class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
<div x-show="hostFileModalOpen" x-trap.inert.noscroll="hostFileModalOpen"
x-transition:enter="ease-out duration-100"
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
class="relative w-full py-6 border rounded-sm drop-shadow-sm min-w-full lg:min-w-[36rem] max-w-fit bg-white border-neutral-200 dark:bg-base px-6 dark:border-coolgray-300">
<div class="flex items-center justify-between pb-3">
<h3 class="text-2xl font-bold">Add Host File Mount</h3>
<button @click="hostFileModalOpen=false"
class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 rounded-full dark:text-white hover:bg-neutral-100 dark:hover:bg-coolgray-300 outline-0 focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="relative flex items-center justify-center w-auto"
x-init="$watch('hostFileModalOpen', value => {
if (value) {
$nextTick(() => {
const input = $el.querySelector('input');
input?.focus();
})
}
})">
<form class="flex flex-col w-full gap-2 rounded-sm"
wire:submit='submitHostFileStorage'>
<div class="flex flex-col">
<div>Bind an existing host file into the container. Coolify will not create, edit, load, chmod, or delete the source file.</div>
</div>
<div class="flex flex-col gap-2">
<x-forms.input canGate="update" :canResource="$resource"
placeholder="/etc/nginx/nginx.conf"
id="host_file_storage_source" label="Host File Path" required
helper="Existing file on the host system." />
<x-forms.input canGate="update" :canResource="$resource"
placeholder="/etc/nginx/nginx.conf"
id="host_file_storage_destination" label="Destination Path"
required helper="File location inside the container." />
<x-forms.button canGate="update" :canResource="$resource" type="submit">
Add
</x-forms.button>
</div>
</form>
</div>
</div>
</div>
</template>
{{-- Directory Modal --}}
<template x-teleport="body">
<div x-show="directoryModalOpen" @keydown.window.escape="directoryModalOpen=false"
@@ -184,6 +184,7 @@
@endif
</div>
<x-forms.input instantSave id="comment" label="Comment"
placeholder="{{ $isMagicVariable ? 'This env cannot be edited manually, it is handled by Coolify.' : '' }}"
helper="Add a note to document what this environment variable is used for." maxlength="256" />
</div>
@endif
@@ -191,16 +192,23 @@
<div class="flex flex-col w-full gap-2">
<div class="flex flex-col w-full gap-2 lg:flex-row">
<x-forms.input disabled id="key" />
<x-forms.env-var-input
disabled
type="password"
id="value"
:availableVars="$isSharedVariable ? [] : $this->availableSharedVariables"
:projectUuid="data_get($parameters, 'project_uuid')"
:environmentUuid="data_get($parameters, 'environment_uuid')"
:serverUuid="data_get($parameters, 'server_uuid')" />
@if ($is_shared)
<x-forms.input disabled type="password" id="real_value" />
@if ($isValueHidden)
<div class="w-full">
<input disabled type="text" value="Hidden (only admins can view)"
class="input italic !text-neutral-500 dark:!text-neutral-500" />
</div>
@else
<x-forms.env-var-input
disabled
type="password"
id="value"
:availableVars="$isSharedVariable ? [] : $this->availableSharedVariables"
:projectUuid="data_get($parameters, 'project_uuid')"
:environmentUuid="data_get($parameters, 'environment_uuid')"
:serverUuid="data_get($parameters, 'server_uuid')" />
@if ($is_shared)
<x-forms.input disabled type="password" id="real_value" />
@endif
@endif
</div>
<x-forms.input disabled id="comment" label="Comment"
@@ -3,12 +3,12 @@
<h2>Healthcheck</h2>
<x-forms.button canGate="update" :canResource="$resource" type="submit">Save</x-forms.button>
@if (!$healthCheckEnabled)
<x-modal-confirmation title="Confirm Healthcheck Enable?" buttonTitle="Enable Healthcheck"
submitAction="toggleHealthcheck" :actions="['Enable healthcheck for this resource.']"
warningMessage="If the health check fails, your application will become inaccessible. Please review the <a href='https://coolify.io/docs/knowledge-base/health-checks' target='_blank' class='underline text-white'>Health Checks</a> guide before proceeding!"
step2ButtonText="Enable Healthcheck" :confirmWithText="false" :confirmWithPassword="false"
isHighlightedButton>
</x-modal-confirmation>
<x-modal-confirmation :disabled="!auth()->user()->can('update', $resource)" :authDisabled="!auth()->user()->can('update', $resource)" title="Confirm Healthcheck Enable?" buttonTitle="Enable Healthcheck"
submitAction="toggleHealthcheck" :actions="['Enable healthcheck for this resource.']"
warningMessage="If the health check fails, your application will become inaccessible. Please review the <a href='https://coolify.io/docs/knowledge-base/health-checks' target='_blank' class='underline text-white'>Health Checks</a> guide before proceeding!"
step2ButtonText="Enable Healthcheck" :confirmWithText="false" :confirmWithPassword="false"
isHighlightedButton>
</x-modal-confirmation>
@else
<x-forms.button canGate="update" :canResource="$resource" wire:click="toggleHealthcheck">Disable Healthcheck</x-forms.button>
@endif
@@ -1,6 +1,4 @@
<div class="w-full max-h-[70vh] overflow-y-auto pr-1 -mt-4">
<div class="pb-4 text-sm dark:text-neutral-400">Identifiers for this resource. Read-only</div>
<div class="w-full max-h-[70vh] overflow-y-auto pr-1 pt-1">
<div class="flex flex-col gap-6">
<div>
<h3>Resource</h3>
@@ -101,16 +101,10 @@
</div>
</div>
</div>
@else
<x-callout type="warning" title="Access Restricted">
You don't have permission to clone resources. Contact your team administrator to request access.
</x-callout>
@endcan
<h3 class="pt-4">Move Resource</h3>
<div class="pb-4">Transfer this resource between projects and environments.</div>
<h3 class="pt-4">Move Resource</h3>
<div class="pb-4">Transfer this resource between projects and environments.</div>
@can('update', $resource)
@if ($projects->count() > 0)
<div class="space-y-4">
<div class="flex flex-col lg:flex-row gap-4">
@@ -160,9 +154,8 @@
</div>
@endif
@else
<x-callout type="warning" title="Access Restricted">
You don't have permission to move resources between projects or environments. Contact your team
administrator to request access.
<x-callout type="danger" title="Insufficient Permissions">
You don't have permission to modify this resource. Contact your team administrator for access.
</x-callout>
@endcan
</div>
@@ -3,44 +3,47 @@
<div class="flex flex-col gap-2 pb-2">
<div class="flex gap-2 items-end">
<h2>Task {{ $task->name }}</h2>
<x-forms.button type="submit">
<x-forms.button canGate="update" :canResource="$resource" type="submit">
Save
</x-forms.button>
@if ($resource->isRunning())
<x-forms.button type="button" wire:click="executeNow">
Execute Now
</x-forms.button>
@can('update', $resource)
<x-forms.button type="button" wire:click="executeNow">
Execute Now
</x-forms.button>
@endcan
@endif
@if (!$isEnabled)
<x-forms.button wire:click="toggleEnabled" isHighlighted>Enable Task</x-forms.button>
@else
<x-forms.button wire:click="toggleEnabled">Disable Task</x-forms.button>
@endif
<x-modal-confirmation title="Confirm Scheduled Task Deletion?" isErrorButton buttonTitle="Delete"
submitAction="delete({{ $task->id }})" :actions="['The selected scheduled task will be permanently deleted.']" confirmationText="{{ $task->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Scheduled Task Name below"
shortConfirmationLabel="Scheduled Task Name" :confirmWithPassword="false"
step2ButtonText="Permanently Delete" />
@can('update', $resource)
@if (!$isEnabled)
<x-forms.button wire:click="toggleEnabled" isHighlighted>Enable Task</x-forms.button>
@else
<x-forms.button wire:click="toggleEnabled">Disable Task</x-forms.button>
@endif
<x-modal-confirmation title="Confirm Scheduled Task Deletion?" isErrorButton buttonTitle="Delete"
submitAction="delete({{ $task->id }})" :actions="['The selected scheduled task will be permanently deleted.']" confirmationText="{{ $task->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Scheduled Task Name below"
shortConfirmationLabel="Scheduled Task Name" :confirmWithPassword="false"
step2ButtonText="Permanently Delete" />
@endcan
</div>
<h3 class="pt-4">Configuration</h3>
<div class="flex gap-2 w-full">
<x-forms.input placeholder="Name" id="name" label="Name" required />
<x-forms.input placeholder="0 0 * * * or daily" id="frequency" label="Frequency"
<x-forms.input :disabled="!auth()->user()->can('update', $resource)" placeholder="Name" id="name" label="Name" required />
<x-forms.input :disabled="!auth()->user()->can('update', $resource)" placeholder="0 0 * * * or daily" id="frequency" label="Frequency"
helper="You can use every_minute, hourly, daily, weekly, monthly, yearly or a cron expression." required />
<x-forms.input type="number" placeholder="300" id="timeout"
<x-forms.input :disabled="!auth()->user()->can('update', $resource)" type="number" placeholder="300" id="timeout"
helper="Maximum execution time in seconds (60-36000)." label="Timeout (seconds)" required />
@if ($type === 'application')
<x-forms.input placeholder="php"
<x-forms.input :disabled="!auth()->user()->can('update', $resource)" placeholder="php"
helper="You can leave this empty if your resource only has one container." id="container"
label="Container name" />
@elseif ($type === 'service')
<x-forms.input placeholder="php"
<x-forms.input :disabled="!auth()->user()->can('update', $resource)" placeholder="php"
helper="You can leave this empty if your resource only has one service in your stack. Otherwise use the stack name, without the random generated ID. So if you have a mysql service in your stack, use mysql."
id="container" label="Service name" />
@endif
</div>
<x-forms.input placeholder="php artisan schedule:run" id="command" label="Command" required />
<x-forms.input :disabled="!auth()->user()->can('update', $resource)" placeholder="php artisan schedule:run" id="command" label="Command" required />
</form>
<div class="pt-4">
@@ -10,8 +10,8 @@
<x-forms.button type="submit">Add</x-forms.button>
</form>
@else
<x-callout type="warning" title="Access Restricted" class="mt-4">
You don't have permission to manage tags. Contact your team administrator to request access.
<x-callout type="danger" title="Insufficient Permissions" class="mt-4">
You don't have permission to manage this resource. Contact your team administrator for access.
</x-callout>
@endcan
@if (data_get($this->resource, 'tags') && count(data_get($this->resource, 'tags')) > 0)
@@ -22,9 +22,9 @@
helper="Need to set a secret to be able to use this webhook. It should match with the secret in GitHub."
label="GitHub Webhook Secret" id="githubManualWebhookSecret"></x-forms.input>
@else
<x-forms.input disabled type="password"
<x-forms.input disabled
helper="Need to set a secret to be able to use this webhook. It should match with the secret in GitHub."
label="GitHub Webhook Secret" id="githubManualWebhookSecret"></x-forms.input>
label="GitHub Webhook Secret" value="Hidden (only admins can view)"></x-forms.input>
@endcan
</div>
<a target="_blank" class="flex hover:no-underline" href="{{ $resource?->gitWebhook }}">
@@ -39,9 +39,9 @@
helper="Need to set a secret to be able to use this webhook. It should match with the secret in GitLab."
label="GitLab Webhook Secret" id="gitlabManualWebhookSecret"></x-forms.input>
@else
<x-forms.input disabled type="password"
<x-forms.input disabled
helper="Need to set a secret to be able to use this webhook. It should match with the secret in GitLab."
label="GitLab Webhook Secret" id="gitlabManualWebhookSecret"></x-forms.input>
label="GitLab Webhook Secret" value="Hidden (only admins can view)"></x-forms.input>
@endcan
</div>
<div class="flex gap-2">
@@ -51,9 +51,9 @@
helper="Need to set a secret to be able to use this webhook. It should match with the secret in Bitbucket."
label="Bitbucket Webhook Secret" id="bitbucketManualWebhookSecret"></x-forms.input>
@else
<x-forms.input disabled type="password"
<x-forms.input disabled
helper="Need to set a secret to be able to use this webhook. It should match with the secret in Bitbucket."
label="Bitbucket Webhook Secret" id="bitbucketManualWebhookSecret"></x-forms.input>
label="Bitbucket Webhook Secret" value="Hidden (only admins can view)"></x-forms.input>
@endcan
</div>
<div class="flex gap-2">
@@ -63,9 +63,9 @@
helper="Need to set a secret to be able to use this webhook. It should match with the secret in Gitea."
label="Gitea Webhook Secret" id="giteaManualWebhookSecret"></x-forms.input>
@else
<x-forms.input disabled type="password"
<x-forms.input disabled
helper="Need to set a secret to be able to use this webhook. It should match with the secret in Gitea."
label="Gitea Webhook Secret" id="giteaManualWebhookSecret"></x-forms.input>
label="Gitea Webhook Secret" value="Hidden (only admins can view)"></x-forms.input>
@endcan
</div>
@can('update', $resource)
@@ -24,20 +24,23 @@
</x-forms.select>
<x-forms.button type="submit">Create</x-forms.button>
</div>
<div class="flex">
Permissions
<span
class="pr-1">:</span>
<div class="flex gap-1 font-bold dark:text-white">
@if ($permissions)
<div class="flex items-center gap-2">
<span>Permissions</span>
<x-helper helper="These permissions will be granted to the token." />
@if ($permissions)
<div class="flex gap-1.5 flex-wrap">
@foreach ($permissions as $permission)
<div>{{ $permission }}</div>
<span
class="px-2 py-0.5 text-xs rounded-sm font-medium {{ $permission === 'root' ? 'bg-red-500/20 text-red-400' : ($permission === 'write' || $permission === 'write:sensitive' ? 'bg-amber-500/20 text-amber-400' : ($permission === 'deploy' ? 'bg-blue-500/20 text-blue-400' : 'bg-neutral-500/20 text-neutral-300')) }}">
{{ $permission }}
</span>
@endforeach
@endif
</div>
</div>
@endif
</div>
<div class="w-64">
<h4>Token Permissions</h4>
<div class="w-96">
@if ($canUseRootPermissions)
<x-forms.checkbox label="root" wire:model.live="permissions" domValue="root"
helper="Root access, be careful!" :checked="in_array('root', $permissions)"></x-forms.checkbox>
@@ -55,13 +58,23 @@
helper="Write access requires admin or owner role" :checked="false"></x-forms.checkbox>
@endif
<x-forms.checkbox label="deploy" wire:model.live="permissions" domValue="deploy"
helper="Can trigger deploy webhooks." :checked="in_array('deploy', $permissions)"></x-forms.checkbox>
<x-forms.checkbox label="read" domValue="read" wire:model.live="permissions" domValue="read"
@if ($canUseDeployPermissions)
<x-forms.checkbox label="deploy" wire:model.live="permissions" domValue="deploy"
helper="Can trigger deploy webhooks." :checked="in_array('deploy', $permissions)"></x-forms.checkbox>
@else
<x-forms.checkbox label="deploy (admin/owner only)" disabled domValue="deploy"
helper="Deploy access requires admin or owner role" :checked="false"></x-forms.checkbox>
@endif
<x-forms.checkbox label="read" wire:model.live="permissions" domValue="read"
:checked="in_array('read', $permissions)"></x-forms.checkbox>
<x-forms.checkbox label="read:sensitive" wire:model.live="permissions" domValue="read:sensitive"
helper="Responses will include secrets, logs, passwords, and compose file contents."
:checked="in_array('read:sensitive', $permissions)"></x-forms.checkbox>
@if ($canUseSensitivePermissions)
<x-forms.checkbox label="read:sensitive" wire:model.live="permissions" domValue="read:sensitive"
helper="Responses will include secrets, logs, passwords, and compose file contents."
:checked="in_array('read:sensitive', $permissions)"></x-forms.checkbox>
@else
<x-forms.checkbox label="read:sensitive (admin/owner only)" disabled domValue="read:sensitive"
helper="Read:sensitive access requires admin or owner role" :checked="false"></x-forms.checkbox>
@endif
@endif
</div>
@if (in_array('root', $permissions))
@@ -70,13 +83,36 @@
</form>
@endcan
@if (session()->has('token'))
<div class="py-4 font-bold dark:text-warning">Please copy this token now. For your security, it won't be shown
again.
<div class="p-4 my-4 border rounded dark:border-coolgray-200 dark:bg-coolgray-100">
<div class="pb-2 font-bold dark:text-warning">Please copy this token now. For your security, it won't
be shown again.</div>
<div class="relative" x-data="{ copied: false, isSecure: window.isSecureContext }">
<input type="text" value="{{ session('token') }}" readonly
class="input !text-white !bg-coolgray-200 font-mono" />
<button x-show="isSecure"
@click.prevent="copied = true; navigator.clipboard.writeText({{ Js::from(session('token')) }}); setTimeout(() => copied = false, 1000)"
class="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-gray-400 hover:text-gray-300 transition-colors"
title="Copy to clipboard">
<svg x-show="!copied" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<svg x-show="copied" class="w-5 h-5 text-green-500" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 13l4 4L19 7" />
</svg>
</button>
</div>
</div>
<div class="pb-4 font-bold dark:text-white"> {{ session('token') }}</div>
@endif
<h3 class="py-4">Issued Tokens</h3>
<div class="flex flex-col">
<div x-data="{ search: '' }">
<div class="flex items-center justify-between py-4">
<h3>Issued Tokens</h3>
@if ($tokens->count() > 1)
<input type="text" x-model="search" placeholder="Filter tokens..." class="input w-64" />
@endif
</div>
<div class="flex flex-col">
<div class="overflow-x-auto">
<div class="inline-block min-w-full">
@@ -94,13 +130,17 @@
</thead>
<tbody>
@forelse ($tokens as $token)
<tr wire:key="token-{{ $token->id }}">
<tr wire:key="token-{{ $token->id }}"
x-show="!search || {{ Js::from(strtolower($token->name).' '.strtolower(implode(' ', $token->abilities ?? []))) }}.includes(search.toLowerCase())">
<td class="px-5 py-4 text-sm whitespace-nowrap">{{ $token->name }}</td>
<td class="px-5 py-4 text-sm whitespace-nowrap">
@if ($token->abilities)
<div class="flex gap-1">
<div class="flex gap-1.5 flex-wrap">
@foreach ($token->abilities as $ability)
<div class="font-bold dark:text-white">{{ $ability }}</div>
<span
class="px-2 py-0.5 text-xs rounded-sm font-medium {{ $ability === 'root' ? 'bg-red-500/20 text-red-400' : ($ability === 'write' || $ability === 'write:sensitive' ? 'bg-amber-500/20 text-amber-400' : ($ability === 'deploy' ? 'bg-blue-500/20 text-blue-400' : 'bg-neutral-500/20 text-neutral-300')) }}">
{{ $ability }}
</span>
@endforeach
</div>
@endif
@@ -71,6 +71,11 @@
</x-callout>
@endif
@if (!$isCloudflareTunnelsEnabled && $server->isFunctional())
@cannot('update', $server)
<x-callout type="danger" title="Insufficient Permissions" class="mb-4">
You don't have permission to configure Cloudflare Tunnel for this server.
</x-callout>
@endcannot
<div class="flex flex-col pb-2">
<h3>Automated </h3>
<a href="https://coolify.io/docs/knowledge-base/cloudflare/tunnels/server-ssh" target="_blank"
@@ -91,10 +96,6 @@
helper="The SSH domain you configured in Cloudflare. Make sure there is no protocol like http(s):// so you provide a FQDN not a URL. <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/cloudflare/tunnels/server-ssh' target='_blank'>Documentation</a>" />
<x-forms.button type="submit" isHighlighted>Continue</x-forms.button>
</form>
@else
<x-callout type="warning" title="Permission Required" class="mb-4">
You don't have permission to configure Cloudflare Tunnel for this server.
</x-callout>
@endcan
</div>
@script
@@ -121,10 +122,6 @@
]" confirmationText="I manually configured Cloudflare Tunnel"
confirmationLabel="Please type the confirmation text to confirm that you manually configured Cloudflare Tunnel."
shortConfirmationLabel="Confirmation text" />
@else
<x-callout type="warning" title="Permission Required" class="mb-4">
You don't have permission to configure Cloudflare Tunnel for this server.
</x-callout>
@endcan
</div>
@endif
@@ -15,7 +15,10 @@
<div class="flex flex-col gap-1.5">
<div class="flex flex-wrap items-center gap-2">
<h1>Server</h1>
@if ($server->proxySet() || $server->isSentinelEnabled())
@php
$showSentinelStatus = $server->isFunctional() && $server->isSentinelEnabled();
@endphp
@if ($server->proxySet() || $showSentinelStatus)
<div data-testid="server-status-summary" class="flex flex-wrap items-center gap-2">
@if ($server->proxySet())
<div class="flex items-center gap-1">
@@ -38,7 +41,7 @@
type="warning" />
</div>
@endif
@if ($server->isSentinelEnabled())
@if ($showSentinelStatus)
@if ($server->isSentinelLive())
<x-status-badge label="Sentinel" status="In sync" type="success" />
@else
@@ -78,7 +81,7 @@
@endif
</a>
@endif
@if ($server->isFunctional() && !$server->isSwarm() && !$server->settings->is_build_server)
@if ($server->isFunctional() && !$server->isSwarm() && !$server->settings->is_build_server && auth()->user()?->can('viewSentinel', $server))
<a class="{{ request()->routeIs('server.sentinel') || request()->routeIs('server.sentinel.*') ? 'dark:text-white' : '' }} flex items-center gap-1" href="{{ route('server.sentinel', [
'server_uuid' => data_get($server, 'uuid'),
]) }}" {{ wireNavigate() }}>
@@ -114,6 +117,7 @@
<div class="order-first sm:order-last">
<div>
@if ($server->proxySet())
@can('manageProxy', $server)
@if ($proxyStatus === 'running')
<div class="flex gap-2">
<div class="mt-1" wire:loading wire:target="loadProxyConfiguration">
@@ -178,6 +182,7 @@
Start Proxy
</button>
@endif
@endcan
@endif
@script
<script>
@@ -174,7 +174,7 @@
</x-forms.button> --}}
</div>
@else
<x-callout type="warning" title="Permission Required" class="mb-4">
<x-callout type="danger" title="Insufficient Permissions" class="mb-4">
You don't have permission to configure proxy settings for this server.
</x-callout>
@endcan
@@ -134,22 +134,22 @@
{{ data_get($resource, 'State') }}
</td>
<td class="flex gap-2 px-5 py-4 text-sm whitespace-nowrap">
@if (data_get($resource, 'State') === 'running')
<x-forms.button
wire:click="restartUnmanaged('{{ data_get($resource, 'ID') }}')"
wire:key="{{ data_get($resource, 'ID') }}">Restart</x-forms.button>
<x-forms.button isError
wire:click="stopUnmanaged('{{ data_get($resource, 'ID') }}')"
wire:key="{{ data_get($resource, 'ID') }}">Stop</x-forms.button>
@elseif (data_get($resource, 'State') === 'exited')
<x-forms.button
wire:click="startUnmanaged('{{ data_get($resource, 'ID') }}')"
wire:key="{{ data_get($resource, 'ID') }}">Start</x-forms.button>
@elseif (data_get($resource, 'State') === 'restarting')
<x-forms.button
wire:click="stopUnmanaged('{{ data_get($resource, 'ID') }}')"
wire:key="{{ data_get($resource, 'ID') }}">Stop</x-forms.button>
@endif
@if (data_get($resource, 'State') === 'running')
<x-forms.button canGate="update" :canResource="$server"
wire:click="restartUnmanaged('{{ data_get($resource, 'ID') }}')"
wire:key="{{ data_get($resource, 'ID') }}">Restart</x-forms.button>
<x-forms.button canGate="update" :canResource="$server" isError
wire:click="stopUnmanaged('{{ data_get($resource, 'ID') }}')"
wire:key="{{ data_get($resource, 'ID') }}">Stop</x-forms.button>
@elseif (data_get($resource, 'State') === 'exited')
<x-forms.button canGate="update" :canResource="$server"
wire:click="startUnmanaged('{{ data_get($resource, 'ID') }}')"
wire:key="{{ data_get($resource, 'ID') }}">Start</x-forms.button>
@elseif (data_get($resource, 'State') === 'restarting')
<x-forms.button canGate="update" :canResource="$server"
wire:click="stopUnmanaged('{{ data_get($resource, 'ID') }}')"
wire:key="{{ data_get($resource, 'ID') }}">Stop</x-forms.button>
@endif
</td>
</tr>
@endforeach
@@ -120,6 +120,19 @@
@else
You can't use this server until it is validated.
@endif
@if ($this->limaStartCommand)
<div
class="mt-4 rounded border border-coolgray-200 bg-coolgray-100 p-3 text-sm dark:border-coolgray-300 dark:bg-coolgray-100">
<div class="font-semibold">Start this Lima VM locally</div>
<div class="mt-1 dark:text-neutral-400">
Run this from the Coolify repository root before validating this server:
</div>
<code
class="mt-2 block overflow-x-auto rounded bg-black px-3 py-2 font-mono text-xs text-white">
{{ $this->limaStartCommand }}
</code>
</div>
@endif
@if ($isValidating)
<div x-data="{ slideOverOpen: true }">
<x-slide-over closeWithX fullScreen>
@@ -27,7 +27,7 @@
<x-forms.input type="password" label="Password" readonly id="postgres_password" />
</div>
</div>
<livewire:project.database.backup-edit :backup="$backup" :s3s="$s3s" :status="data_get($database, 'status')" />
<livewire:project.database.backup-edit :backup="$backup" :available-s3-storages="$s3s" :status="data_get($database, 'status')" />
<div class="py-4">
<livewire:project.database.backup-executions :backup="$backup" />
</div>
@@ -114,7 +114,7 @@
<span class="text-left menu-item-label" :class="collapsed && 'lg:hidden'">What's New</span>
@if ($unreadCount > 0)
<span
class="absolute top-0 right-0 bg-error text-white text-[10px] rounded-full min-w-4 h-4 px-1 flex items-center justify-center"
class="absolute right-2 top-1/2 -translate-y-1/2 bg-error text-white text-[10px] rounded-full min-w-4 h-4 px-1 flex items-center justify-center"
aria-label="{{ $unreadCount }} unread changelog {{ Str::plural('entry', $unreadCount) }}">
{{ $unreadCount > 9 ? '9+' : $unreadCount }}
</span>
@@ -70,9 +70,18 @@
environments!
</x-callout>
@endif
<h4 class="pt-4">Webhook/S3 Endpoint Controls</h4>
<x-forms.textarea id="webhook_allowed_internal_hosts" rows="4"
label="Allowed Internal Webhook/S3 Targets"
helper="Optional instance-level allowlist for webhook and S3 endpoint destinations. Supports exact hostnames, IPs, and CIDR ranges separated by commas or new lines."
placeholder="hooks.company.local, 10.50.0.0/16, 192.168.10.20" />
<div class="md:w-96">
<x-forms.checkbox id="webhook_allow_localhost" label="Allow Localhost Webhook/S3 Targets"
helper="Allows localhost/loopback targets only when they are also listed above. Use only for local development or trusted single-instance setups." />
</div>
<h4 class="pt-4">MCP Server</h4>
<div class="md:w-96">
<x-forms.checkbox instantSave id="is_mcp_server_enabled" label="Enable MCP Server"
<x-forms.checkbox instantSave id="is_mcp_server_enabled" label="Enable MCP Server Instance-wide"
helper="Exposes a Streamable HTTP Model Context Protocol endpoint at /mcp for AI clients (Claude Desktop, Cursor, etc.). Authenticates via Sanctum API tokens (Security > API Tokens). Requires API Access to be enabled." />
</div>
@if ($is_mcp_server_enabled)
@@ -81,12 +81,6 @@
placeholder="2001:db8::1" autocomplete="new-password" />
</div>
@if($buildActivityId)
<div class="w-full mt-4">
<livewire:activity-monitor header="Building Helper Image" :activityId="$buildActivityId"
:fullHeight="false" />
</div>
@endif
@if(isDev())
<x-forms.input canGate="update" :canResource="$settings" id="dev_helper_version" label="Dev Helper Version (Development Only)"
helper="Override the default coolify-helper image version. Leave empty to use the default version from config ({{ config('constants.coolify.helper_version') }}). Examples: 1.0.11, latest, dev"
@@ -44,6 +44,15 @@
<x-forms.input required label="Frequency (cron expression)" disabled placeholder="disabled"
helper="Frequency (cron expression) (automatically update coolify).<br>You can use every_minute, hourly, daily, weekly, monthly, yearly.<br><br>Default is every day at 00:00" />
@endif
<h4 class="pt-4">Docker Registry</h4>
<div class="md:w-96">
<x-forms.select id="docker_registry_url" label="Docker Registry"
helper="The Docker registry used to pull Coolify images during updates.<br>Switch to Docker Hub if you experience rate limiting with GitHub Container Registry.">
<option value="docker.io">Docker Hub (docker.io)</option>
<option value="ghcr.io">GitHub Container Registry (ghcr.io)</option>
</x-forms.select>
</div>
</div>
</form>
@@ -63,7 +63,7 @@
</x-forms.button>
</form>
@else
<x-callout type="warning" title="Permission Required">
<x-callout type="danger" title="Insufficient Permissions">
You don't have permission to create new GitHub Apps. Please contact your team administrator for access.
</x-callout>
@endcan
@@ -24,7 +24,7 @@
</form>
</div>
@else
<x-callout type="warning" title="Permission Required">
<x-callout type="danger" title="Insufficient Permissions">
You don't have permission to create new S3 storage configurations. Please contact your team administrator for
access.
</x-callout>
@@ -10,10 +10,15 @@
<x-forms.input canGate="update" :canResource="$storage" required label="Region" id="region" />
</div>
<div class="flex gap-2">
<x-forms.input canGate="update" :canResource="$storage" required type="password" label="Access Key"
id="key" />
<x-forms.input canGate="update" :canResource="$storage" required type="password" label="Secret Key"
id="secret" />
@if ($isPasswordHiddenForMember)
<x-forms.input label="Access Key" disabled value="Hidden (only admins can view)" />
<x-forms.input label="Secret Key" disabled value="Hidden (only admins can view)" />
@else
<x-forms.input canGate="update" :canResource="$storage" required type="password" label="Access Key"
id="key" />
<x-forms.input canGate="update" :canResource="$storage" required type="password" label="Secret Key"
id="secret" />
@endif
</div>
@can('validateConnection', $storage)
<x-forms.button class="mt-4" isHighlighted wire:click="testConnection">
@@ -54,7 +54,7 @@
<div class="flex gap-2">
<h1>Subscription</h1>
</div>
<x-callout type="warning" title="Permission Required">
<x-callout type="danger" title="Insufficient Permissions">
You are not an admin so you cannot manage your Team's subscription. If this does not make sense, please
<span class="underline cursor-pointer dark:text-white" wire:click="help">contact us</span>.
</x-callout>
@@ -13,6 +13,8 @@
<div class="flex items-end gap-2 pb-6">
<x-forms.input id="name" label="Name" required canGate="update" :canResource="$team" />
<x-forms.input id="description" label="Description" canGate="update" :canResource="$team" />
<x-forms.checkbox id="is_mcp_server_enabled" label="Enable MCP Server" canGate="update" :canResource="$team"
helper="Allows this team's API tokens to use the instance MCP endpoint when MCP is enabled instance-wide." />
@can('update', $team)
<x-forms.button type="submit">
Save
@@ -1,20 +1,22 @@
@can('manageInvitations', currentTeam())
<form wire:submit='viaLink' class="flex gap-2 flex-col lg:flex-row items-end">
<div class="flex flex-1 lg:w-fit w-full gap-2">
<x-forms.input id="email" type="email" label="Email" name="email" placeholder="Email" required />
<x-forms.select id="role" name="role" label="Role">
@if (auth()->user()->role() === 'owner')
<option value="owner">Owner</option>
<div>
@can('manageInvitations', currentTeam())
<form wire:submit='viaLink' class="flex gap-2 flex-col lg:flex-row items-end">
<div class="flex flex-1 lg:w-fit w-full gap-2">
<x-forms.input id="email" type="email" label="Email" name="email" placeholder="Email" required />
<x-forms.select id="role" name="role" label="Role">
@if (auth()->user()->role() === 'owner')
<option value="owner">Owner</option>
@endif
<option value="admin">Admin</option>
<option value="member">Member</option>
</x-forms.select>
</div>
<div class="flex gap-2 lg:w-fit w-full">
<x-forms.button type="submit">Generate Invitation Link</x-forms.button>
@if (is_transactional_emails_enabled())
<x-forms.button wire:click.prevent='viaEmail'>Send Invitation via Email</x-forms.button>
@endif
<option value="admin">Admin</option>
<option value="member">Member</option>
</x-forms.select>
</div>
<div class="flex gap-2 lg:w-fit w-full">
<x-forms.button type="submit">Generate Invitation Link</x-forms.button>
@if (is_transactional_emails_enabled())
<x-forms.button wire:click.prevent='viaEmail'>Send Invitation via Email</x-forms.button>
@endif
</div>
</form>
@endcan
</div>
</form>
@endcan
</div>
+2 -2
View File
@@ -13,7 +13,7 @@
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M19.5 13.572l-7.5 7.428l-7.5 -7.428m0 0a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" />
</svg>
<span class="text-left menu-item-label" :class="collapsed && 'lg:hidden'">In progress</span>
<span class="text-left menu-item-label sidebar-collapsed-label">In progress</span>
</button>
<button title="Upgrade" aria-label="Upgrade" class="menu-item cursor-pointer" @click="modalOpen=true" x-show="!showProgress">
<svg xmlns="http://www.w3.org/2000/svg" class="text-pink-500 transition-colors menu-item-icon hover:text-pink-300"
@@ -25,7 +25,7 @@
<path d="M9 21h6" />
<path d="M9 18h6" />
</svg>
<span class="text-left menu-item-label" :class="collapsed && 'lg:hidden'">Upgrade</span>
<span class="text-left menu-item-label sidebar-collapsed-label">Upgrade</span>
</button>
<template x-teleport="body">
<div x-show="modalOpen"