Merge remote-tracking branch 'origin/next' into feature/vultr-cloud-provider

This commit is contained in:
Andras Bacsai committed 2026-07-07 13:44:37 +02:00
commit c58b4fd69c
718 files changed
+30292 -7279

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,34 +3,84 @@
Advanced
</x-slot>
@if ($application->status === 'running')
<div class="dropdown-iteme" wire:click='force_deploy_without_cache'>
<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>
@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
<div class="dropdown-item" wire:click='deploy(true)'>
<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>
@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,67 +365,6 @@
<li>
<livewire:settings-dropdown trigger="changelog-sidebar" />
</li>
<li>
<div class="menu-item" title="Theme" aria-label="Theme switcher" :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="menu-item-label">Theme</span>
<div class="ml-auto flex items-center gap-0.5 rounded-sm bg-neutral-100 p-0.5 dark:bg-coolgray-200">
<button type="button" @click.stop="setTheme('light')" title="Light" aria-label="Use light theme"
class="grid size-6 place-items-center rounded-sm text-xs hover:bg-white hover:text-coollabs dark:hover:bg-base dark:hover:text-warning"
:class="theme === 'light' && 'bg-white text-coollabs shadow-sm dark:bg-base dark:text-warning'">
<svg class="size-4" 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>
</button>
<button type="button" @click.stop="setTheme('system')" title="System default" aria-label="Use system theme"
class="grid size-6 place-items-center rounded-sm text-xs hover:bg-white hover:text-coollabs dark:hover:bg-base dark:hover:text-warning"
:class="theme === 'system' && 'bg-white text-coollabs shadow-sm dark:bg-base dark:text-warning'">
<svg class="size-4" 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>
<button type="button" @click.stop="setTheme('dark')" title="Dark" aria-label="Use dark theme"
class="grid size-6 place-items-center rounded-sm text-xs hover:bg-white hover:text-coollabs dark:hover:bg-base dark:hover:text-warning"
:class="theme === 'dark' && 'bg-white text-coollabs shadow-sm dark:bg-base dark:text-warning'">
<svg class="size-4" 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>
</button>
</div>
</div>
<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>
@@ -4,7 +4,7 @@
'isImportSupported' => false,
])
<div class="sub-menu-wrapper">
<div class="sub-menu-wrapper hidden md:flex">
<a class="sub-menu-item"
class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-active' : '' }}"
{{ wireNavigate() }}
@@ -3,7 +3,7 @@
Advanced
</x-slot>
@if (str($service->status)->contains('running'))
<div class="dropdown-item" @click="$wire.dispatch('pullAndRestartEvent')">
<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="$wire.dispatch('forceDeployEvent')">
<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="$wire.dispatch('forceDeployEvent')">
<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" wire:click='stop(true)''>
<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"
@@ -0,0 +1,38 @@
@props([
'label' => null,
'status' => null,
'type' => 'neutral',
'as' => 'span',
])
@php
$typeClasses = [
'neutral' => 'border-neutral-200 bg-neutral-100 text-black dark:border-coolgray-300 dark:bg-coolgray-200 dark:text-white',
'success' => 'border-green-200 bg-green-50 text-green-800 dark:border-green-900 dark:bg-green-950/30 dark:text-green-300',
'warning' => 'border-yellow-300 bg-yellow-50 text-yellow-900 dark:border-yellow-800 dark:bg-yellow-950/30 dark:text-yellow-200',
'error' => 'border-red-300 bg-red-50 text-red-800 dark:border-red-900 dark:bg-red-950/30 dark:text-red-300',
];
@endphp
@if ($as === 'button')
<button {{ $attributes->class([
'inline-flex h-5 max-w-full items-center gap-1 rounded-sm border px-1.5 text-xs font-medium leading-4 transition-colors',
$typeClasses[$type] ?? $typeClasses['neutral'],
])->merge(['type' => 'button']) }}>
{{ collect([$label, $status])->filter()->join(' ') }}
</button>
@elseif ($as === 'a')
<a {{ $attributes->class([
'inline-flex h-5 max-w-full items-center gap-1 rounded-sm border px-1.5 text-xs font-medium leading-4 transition-colors',
$typeClasses[$type] ?? $typeClasses['neutral'],
]) }}>
{{ collect([$label, $status])->filter()->join(' ') }}
</a>
@else
<span {{ $attributes->class([
'inline-flex h-5 max-w-full items-center gap-1 rounded-sm border px-1.5 text-xs font-medium leading-4',
$typeClasses[$type] ?? $typeClasses['neutral'],
]) }}>
{{ collect([$label, $status])->filter()->join(' ') }}
</span>
@endif
@@ -2,32 +2,18 @@
'status' => 'Degraded',
])
@php
// Handle both colon format (backend) and parentheses format (from services.blade.php)
// degraded:unhealthy → Degraded (unhealthy)
// degraded (unhealthy) → degraded (unhealthy) (already formatted, display as-is)
if (str($status)->contains('(')) {
// Already in parentheses format from services.blade.php - use as-is
$displayStatus = $status;
$healthStatus = str($status)->after('(')->before(')')->trim()->value();
} elseif (str($status)->contains(':') && !str($status)->startsWith('Proxy')) {
// Colon format from backend - transform it
$healthStatus = null;
} elseif (str($status)->contains(':') && ! str($status)->startsWith('Proxy')) {
$parts = explode(':', $status);
$displayStatus = str($parts[0])->headline();
$displayStatus = str($parts[0])->headline()->value();
$healthStatus = $parts[1] ?? null;
} else {
// Simple status without health
$displayStatus = str($status)->headline();
$displayStatus = str($status)->headline()->value();
$healthStatus = null;
}
$badgeStatus = $healthStatus ? "{$displayStatus} ({$healthStatus})" : $displayStatus;
@endphp
<div class="flex items-center" >
<x-loading wire:loading.delay.longer />
<span wire:loading.remove.delay.longer class="flex items-center">
<div class="badge badge-warning"></div>
<div class="pl-2 pr-1 text-xs font-bold dark:text-warning">{{ $displayStatus }}</div>
@if ($healthStatus && !str($displayStatus)->contains('('))
<div class="text-xs dark:text-warning">({{ $healthStatus }})</div>
@endif
</span>
</div>
<x-status-badge status="{{ $badgeStatus }}" type="warning" />
@@ -18,33 +18,18 @@
<x-status.stopped :status="$resource->status" />
@endif
@if (isset($resource->restart_count) && $resource->restart_count > 0 && (!str($resource->status)->startsWith('exited') || $stoppedAfterRestartLimit))
<div class="flex items-center">
<span class="text-xs dark:text-warning" title="Container has restarted {{ $resource->restart_count }} time{{ $resource->restart_count > 1 ? 's' : '' }}. Last restart: {{ $resource->last_restart_at?->diffForHumans() }}">
({{ $resource->restart_count }}x restarts)
</span>
</div>
<x-status-badge status="{{ $resource->restart_count }}x restarts" type="warning"
title="Container has restarted {{ $resource->restart_count }} time{{ $resource->restart_count > 1 ? 's' : '' }}. Last restart: {{ $resource->last_restart_at?->diffForHumans() }}" />
@endif
@if ($stoppedAfterRestartLimit)
<div class="flex items-center">
<span class="text-xs dark:text-warning" title="Container has crashed and Coolify stopped it after {{ $resource->restart_count }} restart attempts.">
Stopped after reaching restart limit ({{ $resource->restart_count }}/{{ $resource->max_restart_count }}).
</span>
</div>
<x-status-badge status="Stopped after reaching restart limit ({{ $resource->restart_count }}/{{ $resource->max_restart_count }})."
type="warning"
title="Container has crashed and Coolify stopped it after {{ $resource->restart_count }} restart attempts." />
@endif
@if (!str($resource->status)->contains('exited') && $showRefreshButton)
<button wire:loading.remove.delay.shortest wire:target="manualCheckStatus" title="Refresh Status" wire:click='manualCheckStatus'
class="dark:hover:fill-white fill-black dark:fill-warning">
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
</svg>
</button>
<button wire:loading.delay.shortest wire:target="manualCheckStatus" title="Refreshing Status" wire:click='manualCheckStatus'
class="dark:hover:fill-white fill-black dark:fill-warning">
<svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
</svg>
</button>
<x-status-badge as="button" wire:target="manualCheckStatus" wire:loading.attr="disabled"
wire:click='manualCheckStatus' status="Refresh" type="neutral" title="Refresh Status"
aria-label="Refresh status"
class="min-w-[4.5rem] justify-center cursor-pointer border-transparent hover:bg-neutral-200 disabled:cursor-wait disabled:opacity-70 dark:hover:bg-coolgray-300" />
@endif
</div>
@@ -5,42 +5,23 @@
'noLoading' => false,
])
@php
// Handle both colon format (backend) and parentheses format (from services.blade.php)
// starting:unknown → Starting (unknown)
// starting (unknown) → starting (unknown) (already formatted, display as-is)
if (str($status)->contains('(')) {
// Already in parentheses format from services.blade.php - use as-is
$displayStatus = $status;
$healthStatus = str($status)->after('(')->before(')')->trim()->value();
} elseif (str($status)->contains(':') && !str($status)->startsWith('Proxy')) {
// Colon format from backend - transform it
$healthStatus = null;
} elseif (str($status)->contains(':') && ! str($status)->startsWith('Proxy')) {
$parts = explode(':', $status);
$displayStatus = str($parts[0])->headline();
$displayStatus = str($parts[0])->headline()->value();
$healthStatus = $parts[1] ?? null;
} else {
// Simple status without health
$displayStatus = str($status)->headline();
$displayStatus = str($status)->headline()->value();
$healthStatus = null;
}
$badgeStatus = $healthStatus ? "{$displayStatus} ({$healthStatus})" : $displayStatus;
@endphp
<div class="flex items-center">
@if (!$noLoading)
<x-loading wire:loading.delay.longer />
@endif
<span wire:loading.remove.delay.longer class="flex items-center">
<div class="badge badge-warning"></div>
<div class="pl-2 pr-1 text-xs font-bold dark:text-warning" @if($title) title="{{$title}}" @endif>
@if ($lastDeploymentLink)
<a href="{{ $lastDeploymentLink }}" target="_blank" class="underline cursor-pointer">
{{ $displayStatus }}
</a>
@else
{{ $displayStatus }}
@endif
</div>
@if ($healthStatus && !str($displayStatus)->contains('('))
<div class="text-xs dark:text-warning">({{ $healthStatus }})</div>
@endif
</span>
</div>
@if ($lastDeploymentLink)
<x-status-badge as="a" href="{{ $lastDeploymentLink }}" target="_blank" status="{{ $badgeStatus }}" type="warning"
title="{{ $title }}" class="cursor-pointer underline" />
@else
<x-status-badge status="{{ $badgeStatus }}" type="warning" title="{{ $title }}" />
@endif
@@ -5,80 +5,47 @@
'noLoading' => false,
])
@php
// Handle both colon format (backend) and parentheses format (from services.blade.php)
// running:healthy → Running (healthy)
// running (healthy) → running (healthy) (already formatted, display as-is)
if (str($status)->contains('(')) {
// Already in parentheses format from services.blade.php - use as-is
$displayStatus = $status;
$healthStatus = str($status)->after('(')->before(')')->trim()->value();
} elseif (str($status)->contains(':') && !str($status)->startsWith('Proxy')) {
// Colon format from backend - transform it
$healthStatus = null;
} elseif (str($status)->contains(':') && ! str($status)->startsWith('Proxy')) {
$parts = explode(':', $status);
$displayStatus = str($parts[0])->headline();
$displayStatus = str($parts[0])->headline()->value();
$healthStatus = $parts[1] ?? null;
} else {
// Simple status without health
$displayStatus = str($status)->headline();
$displayStatus = str($status)->headline()->value();
$healthStatus = null;
}
$badgeStatus = $healthStatus ? "{$displayStatus} ({$healthStatus})" : $displayStatus;
$showUnknownHelper = ! str($status)->startsWith('Proxy') && (str($status)->contains('unknown') || str($healthStatus)->contains('unknown'));
$showUnhealthyHelper = ! str($status)->startsWith('Proxy') && (str($status)->contains('unhealthy') || str($healthStatus)->contains('unhealthy'));
@endphp
<div class="flex items-center">
<div class="flex items-center">
<div wire:loading.delay.longer wire:target="checkProxy(true)" class="badge badge-warning"></div>
<div wire:loading.remove.delay.longer wire:target="checkProxy(true)" class="badge badge-success"></div>
<div class="pl-2 pr-1 text-xs font-bold text-success"
@if ($title) title="{{ $title }}" @endif>
@if ($lastDeploymentLink)
<a href="{{ $lastDeploymentLink }}" target="_blank" class="underline cursor-pointer">
{{ $displayStatus }}
</a>
@else
{{ $displayStatus }}
@endif
</div>
@if ($healthStatus && !str($displayStatus)->contains('('))
<div class="text-xs text-success">({{ $healthStatus }})</div>
@endif
@php
$showUnknownHelper =
!str($status)->startsWith('Proxy') &&
(str($status)->contains('unknown') || str($healthStatus)->contains('unknown'));
$showUnhealthyHelper =
!str($status)->startsWith('Proxy') &&
(str($status)->contains('unhealthy') || str($healthStatus)->contains('unhealthy'));
@endphp
@if ($showUnknownHelper)
<div class="px-2">
<div class="flex items-center gap-1">
@if ($lastDeploymentLink)
<x-status-badge as="a" href="{{ $lastDeploymentLink }}" target="_blank" status="{{ $badgeStatus }}" type="success"
title="{{ $title }}" class="cursor-pointer underline" />
@else
<x-status-badge status="{{ $badgeStatus }}" type="success" title="{{ $title }}" />
@endif
@if ($showUnknownHelper)
<div>
<x-helper
helper="No health check configured. <span class='dark:text-warning text-coollabs'>The resource may be functioning normally.</span><br><br>Traefik and Caddy will route traffic to this container even without a health check. However, configuring a health check is recommended to ensure the resource is ready before receiving traffic.<br><br>More details in the <a href='https://coolify.io/docs/knowledge-base/proxy/traefik/healthchecks' class='underline dark:text-warning text-coollabs' target='_blank'>documentation</a>.">
<x-slot:icon>
<svg class="hidden w-4 h-4 dark:text-warning lg:block" viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16">
</path>
</svg>
<x-status-badge status="No health check" type="warning" class="cursor-help" />
</x-slot:icon>
</x-helper>
</div>
@endif
@if ($showUnhealthyHelper)
<div class="px-2">
@endif
@if ($showUnhealthyHelper)
<div>
<x-helper
helper="Unhealthy state. <span class='dark:text-warning text-coollabs'>The health check is failing.</span><br><br>This resource will <span class='dark:text-warning text-coollabs'>NOT work with Traefik</span> as it expects a healthy state. Your action is required to fix the health check or the underlying issue causing it to fail.<br><br>More details in the <a href='https://coolify.io/docs/knowledge-base/proxy/traefik/healthchecks' class='underline dark:text-warning text-coollabs' target='_blank'>documentation</a>.">
<x-slot:icon>
<svg class="hidden w-4 h-4 dark:text-warning lg:block" viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16">
</path>
</svg>
<x-status-badge status="Unhealthy" type="warning" class="cursor-help" />
</x-slot:icon>
</x-helper>
</div>
@endif
</div>
@endif
</div>
@@ -14,19 +14,9 @@
<x-status.stopped :status="$displayStatus" />
@endif
@if (!str($complexStatus)->contains('exited') && $showRefreshButton)
<button wire:loading.remove.delay.shortest wire:target="manualCheckStatus" title="Refresh Status" wire:click='manualCheckStatus'
class="dark:hover:fill-white fill-black dark:fill-warning">
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
</svg>
</button>
<button wire:loading.delay.shortest wire:target="manualCheckStatus" title="Refreshing Status" wire:click='manualCheckStatus'
class="dark:hover:fill-white fill-black dark:fill-warning">
<svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
</svg>
</button>
<x-status-badge as="button" wire:target="manualCheckStatus" wire:loading.attr="disabled"
wire:click='manualCheckStatus' status="Refresh" type="neutral" title="Refresh Status"
aria-label="Refresh status"
class="min-w-[4.5rem] justify-center cursor-pointer border-transparent hover:bg-neutral-200 disabled:cursor-wait disabled:opacity-70 dark:hover:bg-coolgray-300" />
@endif
</div>
@@ -3,46 +3,12 @@
'noLoading' => false,
])
@php
// Handle both colon format (backend) and parentheses format (from services.blade.php)
// For exited containers, health status is hidden (health checks don't run on stopped containers)
// exited:unhealthy → Exited
// exited (unhealthy) → Exited
if (str($status)->contains('(')) {
// Already in parentheses format from services.blade.php - use as-is
$displayStatus = $status;
$healthStatus = str($status)->after('(')->before(')')->trim()->value();
// Don't show health status for exited containers (health checks don't run on stopped containers)
if (str($displayStatus)->lower()->contains('exited')) {
$displayStatus = str($status)->before('(')->trim()->headline();
$healthStatus = null;
}
$displayStatus = str($status)->before('(')->trim()->headline()->value();
} elseif (str($status)->contains(':')) {
// Colon format from backend - transform it
$parts = explode(':', $status);
$displayStatus = str($parts[0])->headline();
$healthStatus = $parts[1] ?? null;
// Don't show health status for exited containers (health checks don't run on stopped containers)
if (str($displayStatus)->lower()->contains('exited')) {
$healthStatus = null;
}
$displayStatus = str(explode(':', $status)[0])->headline()->value();
} else {
// Simple status without health
$displayStatus = str($status)->headline();
$healthStatus = null;
$displayStatus = str($status)->headline()->value();
}
@endphp
<div class="flex items-center">
@if (!$noLoading)
<x-loading wire:loading.delay.longer />
@endif
<span wire:loading.remove.delay.longer class="flex items-center">
<div class="badge badge-error "></div>
<div class="pl-2 pr-1 text-xs font-bold text-error">{{ $displayStatus }}</div>
@if ($healthStatus && !str($displayStatus)->contains('('))
<div class="text-xs text-error">({{ $healthStatus }})</div>
@endif
</span>
</div>
<x-status-badge status="{{ $displayStatus }}" type="error" />
@@ -48,11 +48,11 @@
characterData: true
});" x-destroy="observer && observer.disconnect()"
@class([
'flex flex-col w-full px-4 py-2 overflow-y-auto bg-white border border-solid rounded-sm dark:text-white dark:bg-coolgray-100 scrollbar border-neutral-300 dark:border-coolgray-300',
'flex flex-col w-full min-w-0 max-w-full px-4 py-2 overflow-y-auto bg-white border border-solid rounded-sm dark:text-white dark:bg-coolgray-100 scrollbar border-neutral-300 dark:border-coolgray-300',
'flex-1 min-h-0' => $fullHeight,
'max-h-96' => !$fullHeight,
])>
<pre class="font-logs whitespace-pre-wrap" @if ($isPollingActive) wire:poll.1000ms="polling" @endif>{{ RunRemoteProcess::decodeOutput($activity) }}</pre>
<pre class="font-logs min-w-0 max-w-full whitespace-pre-wrap wrap-anywhere" @if ($isPollingActive) wire:poll.1000ms="polling" @endif>{{ RunRemoteProcess::decodeOutput($activity) }}</pre>
</div>
@else
@if ($showWaiting)
+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
@@ -632,7 +632,7 @@
@foreach ($searchResults as $result)
@if (!isset($result['is_creatable_suggestion']))
<a href="{{ $result['link'] ?? '#' }}"
class="search-result-item block px-4 py-3 hover:bg-neutral-50 dark:hover:bg-coolgray-200 transition-colors focus:outline-none focus:bg-warning-50 dark:focus:bg-warning-900/20 border-transparent hover:border-coollabs focus:border-warning-500 dark:focus:border-warning-400">
class="search-result-item block px-4 py-3 hover:bg-neutral-100 dark:hover:bg-coolgray-200 transition-colors focus:outline-none focus:bg-neutral-100 dark:focus:bg-coolgray-200 focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-coollabs dark:focus-visible:ring-warning">
<div class="flex items-center justify-between gap-3">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
@@ -696,12 +696,12 @@
<!-- Category Items -->
@foreach ($items as $item)
<button type="button" wire:click="navigateToResource('{{ $item['type'] }}')"
class="search-result-item w-full text-left block px-4 py-3 hover:bg-warning-50 dark:hover:bg-warning-900/20 transition-colors focus:outline-none focus:bg-warning-100 dark:focus:bg-warning-900/30 border-transparent hover:border-warning-500 focus:border-warning-500">
class="search-result-item w-full text-left block px-4 py-3 hover:bg-neutral-100 dark:hover:bg-coolgray-200 transition-colors focus:outline-none focus:bg-neutral-100 dark:focus:bg-coolgray-200 focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-coollabs dark:focus-visible:ring-warning">
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3 flex-1 min-w-0">
@if (! empty($item['logo']))
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center overflow-hidden">
<img src="{{ asset($item['logo']) }}" alt="{{ $item['name'] }}" class="w-7 h-7 object-contain">
<img src="{{ asset($item['logo']) }}" alt="{{ $item['name'] }}" class="w-8 h-8 object-contain">
</div>
@else
<div
@@ -755,7 +755,7 @@
</template>
<template x-for="(result, index) in searchResults" :key="index">
<a :href="result.link || '#'"
class="search-result-item block px-4 py-3 hover:bg-neutral-50 dark:hover:bg-coolgray-200 transition-colors focus:outline-none focus:bg-warning-50 dark:focus:bg-warning-900/20 border-transparent hover:border-coollabs focus:border-warning-500 dark:focus:border-warning-400">
class="search-result-item block px-4 py-3 hover:bg-neutral-100 dark:hover:bg-coolgray-200 transition-colors focus:outline-none focus:bg-neutral-100 dark:focus:bg-coolgray-200 focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-coollabs dark:focus-visible:ring-warning">
<div class="flex items-center justify-between gap-3">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
@@ -811,12 +811,12 @@
<template x-for="item in items" :key="item.type">
<button type="button" @click="$wire.navigateToResource(item.type)"
class="search-result-item w-full text-left block px-4 py-3 hover:bg-warning-50 dark:hover:bg-warning-900/20 transition-colors focus:outline-none focus:bg-warning-100 dark:focus:bg-warning-900/30 border-transparent hover:border-warning-500 focus:border-warning-500">
class="search-result-item w-full text-left block px-4 py-3 hover:bg-neutral-100 dark:hover:bg-coolgray-200 transition-colors focus:outline-none focus:bg-neutral-100 dark:focus:bg-coolgray-200 focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-coollabs dark:focus-visible:ring-warning">
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3 flex-1 min-w-0">
<template x-if="item.logo">
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center overflow-hidden">
<img :src="'/' + item.logo" :alt="item.name" class="w-7 h-7 object-contain">
<img :src="'/' + item.logo" :alt="item.name" class="w-8 h-8 object-contain">
</div>
</template>
<template x-if="!item.logo">
@@ -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>
@@ -6,55 +6,128 @@
<livewire:project.shared.configuration-checker :resource="$application" />
<livewire:project.application.heading :application="$application" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<div class="sub-menu-wrapper">
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">General</span></a>
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.advanced', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Advanced</span></a>
@if ($application->destination->server->isSwarm())
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.swarm', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Swarm</span>
@php
$applicationRouteParameters = [
'project_uuid' => $project->uuid,
'environment_uuid' => $environment->uuid,
'application_uuid' => $application->uuid,
];
$configurationMenuItems = [
[
'label' => 'General',
'route' => 'project.application.configuration',
'active' => $currentRoute === 'project.application.configuration',
],
[
'label' => 'Advanced',
'route' => 'project.application.advanced',
'active' => $currentRoute === 'project.application.advanced',
],
[
'label' => 'Swarm',
'route' => 'project.application.swarm',
'active' => $currentRoute === 'project.application.swarm',
'visible' => $application->destination->server->isSwarm(),
],
[
'label' => 'Environment Variables',
'route' => 'project.application.environment-variables',
'active' => $currentRoute === 'project.application.environment-variables',
],
[
'label' => 'Persistent Storage',
'route' => 'project.application.persistent-storage',
'active' => $currentRoute === 'project.application.persistent-storage',
],
[
'label' => 'Git Source',
'route' => 'project.application.source',
'active' => $currentRoute === 'project.application.source',
'visible' => $application->git_based(),
],
[
'label' => 'Servers',
'route' => 'project.application.servers',
'active' => $currentRoute === 'project.application.servers',
'badge' => true,
],
[
'label' => 'Scheduled Tasks',
'route' => 'project.application.scheduled-tasks.show',
'active' => str($currentRoute)->startsWith('project.application.scheduled-tasks'),
],
[
'label' => 'Webhooks',
'route' => 'project.application.webhooks',
'active' => $currentRoute === 'project.application.webhooks',
],
[
'label' => 'Preview Deployments',
'route' => 'project.application.preview-deployments',
'active' => $currentRoute === 'project.application.preview-deployments',
'visible' => $application->git_based() || $application->build_pack === 'dockerimage',
],
[
'label' => 'Healthcheck',
'route' => 'project.application.healthcheck',
'active' => $currentRoute === 'project.application.healthcheck',
'visible' => $application->build_pack !== 'dockercompose',
],
[
'label' => 'Rollback',
'route' => 'project.application.rollback',
'active' => $currentRoute === 'project.application.rollback',
],
[
'label' => 'Resource Limits',
'route' => 'project.application.resource-limits',
'active' => $currentRoute === 'project.application.resource-limits',
],
[
'label' => 'Resource Operations',
'route' => 'project.application.resource-operations',
'active' => $currentRoute === 'project.application.resource-operations',
],
[
'label' => 'Metrics',
'route' => 'project.application.metrics',
'active' => $currentRoute === 'project.application.metrics',
],
[
'label' => 'Tags',
'route' => 'project.application.tags',
'active' => $currentRoute === 'project.application.tags',
],
[
'label' => 'Danger Zone',
'route' => 'project.application.danger',
'active' => $currentRoute === 'project.application.danger',
],
];
$configurationMenuItems = array_values(array_filter(
$configurationMenuItems,
fn (array $item): bool => $item['visible'] ?? true,
));
@endphp
<div class="flex flex-col h-full gap-4 md:gap-8 md:flex-row">
<div class="sub-menu-wrapper hidden md:flex">
@foreach ($configurationMenuItems as $menuItem)
<a @class([
'sub-menu-item',
'flex items-center gap-2' => $menuItem['badge'] ?? false,
'menu-item-active' => $menuItem['active'],
]) {{ wireNavigate() }} href="{{ route($menuItem['route'], $applicationRouteParameters) }}">
<span class="menu-item-label">{{ $menuItem['label'] }}</span>
@if ($menuItem['badge'] ?? false)
<livewire:project.application.server-status-badge :application="$application" />
@endif
</a>
@endif
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.environment-variables', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Environment Variables</span></a>
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.persistent-storage', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Persistent Storage</span></a>
@if ($application->git_based())
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.source', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Git Source</span></a>
@endif
<a class="sub-menu-item flex items-center gap-2" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.servers', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Servers</span>
<livewire:project.application.server-status-badge :application="$application" />
</a>
<a @class(['sub-menu-item', 'menu-item-active' => str($currentRoute)->startsWith('project.application.scheduled-tasks')]) {{ wireNavigate() }}
href="{{ route('project.application.scheduled-tasks.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Scheduled Tasks</span></a>
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.webhooks', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Webhooks</span></a>
@if ($application->git_based() || $application->build_pack === 'dockerimage')
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.preview-deployments', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Preview Deployments</span></a>
@endif
@if ($application->build_pack !== 'dockercompose')
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.healthcheck', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Healthcheck</span></a>
@endif
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.rollback', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Rollback</span></a>
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.resource-limits', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Resource Limits</span></a>
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.resource-operations', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Resource Operations</span></a>
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.metrics', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Metrics</span></a>
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.tags', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Tags</span></a>
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.danger', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Danger Zone</span></a>
@endforeach
</div>
<div class="w-full sm:flex-grow">
<div class="w-full md:flex-grow">
@if ($currentRoute === 'project.application.configuration')
<livewire:project.application.general :application="$application" />
@elseif ($currentRoute === 'project.application.swarm' && $application->destination->server->isSwarm())
@@ -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">
@@ -1,17 +1,453 @@
<nav wire:poll.10000ms="checkStatus" class="pb-6">
@php
$applicationMenuItems = [
[
'label' => 'Configuration',
'route' => 'project.application.configuration',
'active' => request()->routeIs('project.application.configuration'),
],
[
'label' => 'Deployments',
'route' => 'project.application.deployment.index',
'active' => request()->routeIs('project.application.deployment.index', 'project.application.deployment.show'),
],
[
'label' => 'Logs',
'route' => 'project.application.logs',
'active' => request()->routeIs('project.application.logs'),
],
[
'label' => 'Terminal',
'route' => 'project.application.command',
'active' => request()->routeIs('project.application.command'),
'navigate' => false,
'visible' => ! $application->destination->server->isSwarm() && auth()->user()?->can('canAccessTerminal'),
],
];
$configurationMenuItems = [
[
'label' => 'General',
'route' => 'project.application.configuration',
'active' => request()->routeIs('project.application.configuration'),
],
[
'label' => 'Advanced',
'route' => 'project.application.advanced',
'active' => request()->routeIs('project.application.advanced'),
],
[
'label' => 'Swarm',
'route' => 'project.application.swarm',
'active' => request()->routeIs('project.application.swarm'),
'visible' => $application->destination->server->isSwarm(),
],
[
'label' => 'Environment Variables',
'route' => 'project.application.environment-variables',
'active' => request()->routeIs('project.application.environment-variables'),
],
[
'label' => 'Persistent Storage',
'route' => 'project.application.persistent-storage',
'active' => request()->routeIs('project.application.persistent-storage'),
],
[
'label' => 'Git Source',
'route' => 'project.application.source',
'active' => request()->routeIs('project.application.source'),
'visible' => $application->git_based(),
],
[
'label' => 'Servers',
'route' => 'project.application.servers',
'active' => request()->routeIs('project.application.servers'),
],
[
'label' => 'Scheduled Tasks',
'route' => 'project.application.scheduled-tasks.show',
'active' => request()->routeIs('project.application.scheduled-tasks.show', 'project.application.scheduled-tasks'),
],
[
'label' => 'Webhooks',
'route' => 'project.application.webhooks',
'active' => request()->routeIs('project.application.webhooks'),
],
[
'label' => 'Preview Deployments',
'route' => 'project.application.preview-deployments',
'active' => request()->routeIs('project.application.preview-deployments'),
'visible' => $application->git_based() || $application->build_pack === 'dockerimage',
],
[
'label' => 'Healthcheck',
'route' => 'project.application.healthcheck',
'active' => request()->routeIs('project.application.healthcheck'),
'visible' => $application->build_pack !== 'dockercompose',
],
[
'label' => 'Rollback',
'route' => 'project.application.rollback',
'active' => request()->routeIs('project.application.rollback'),
],
[
'label' => 'Resource Limits',
'route' => 'project.application.resource-limits',
'active' => request()->routeIs('project.application.resource-limits'),
],
[
'label' => 'Resource Operations',
'route' => 'project.application.resource-operations',
'active' => request()->routeIs('project.application.resource-operations'),
],
[
'label' => 'Metrics',
'route' => 'project.application.metrics',
'active' => request()->routeIs('project.application.metrics'),
],
[
'label' => 'Tags',
'route' => 'project.application.tags',
'active' => request()->routeIs('project.application.tags'),
],
[
'label' => 'Danger Zone',
'route' => 'project.application.danger',
'active' => request()->routeIs('project.application.danger'),
],
];
$applicationMenuItems = array_values(array_filter(
$applicationMenuItems,
fn (array $item): bool => $item['visible'] ?? true,
));
$configurationMenuItems = array_values(array_filter(
$configurationMenuItems,
fn (array $item): bool => $item['visible'] ?? true,
));
$activeConfigurationMenuItem = collect($configurationMenuItems)->firstWhere('active', true);
$activeApplicationMenuItem = collect($applicationMenuItems)->firstWhere('active', true);
$activeMobileMenuItem = $activeConfigurationMenuItem
?? $activeApplicationMenuItem
?? $applicationMenuItems[0];
$activeMobileMenuGroup = $activeConfigurationMenuItem ? 'configuration' : 'application';
$activeMobileNavigation = ($activeMobileMenuItem['navigate'] ?? true) ? 'navigate' : 'location';
$activeMobileMenuValue = $activeMobileNavigation.'|'.$activeMobileMenuGroup.'|'.route($activeMobileMenuItem['route'], $parameters);
$mobileSectionChangeHandler = <<<'JS'
const value = $event.target.value;
if (!value) {
return;
}
if (value.startsWith('navigate|')) {
const url = value.split('|').slice(2).join('|');
window.Livewire?.navigate ? window.Livewire.navigate(url) : window.location.href = url;
return;
}
if (value.startsWith('location|')) {
const url = value.split('|').slice(2).join('|');
window.location.href = url;
return;
}
resetToCurrent();
if (value.startsWith('external:')) {
window.open(value.slice(9), '_blank', 'noopener');
return;
}
const action = value.slice(7);
document.getElementById(`application-mobile-${action}-trigger`)?.click();
JS;
@endphp
<x-resources.breadcrumbs :resource="$application" :parameters="$parameters" :title="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" />
<div class="navbar-main">
<div class="w-full md:hidden">
@if (!($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw)))
<div id="application-mobile-actions" class="mt-2 mb-3 md:hidden">
<div class="mb-1 text-xs font-semibold uppercase tracking-wide text-neutral-500 dark:text-neutral-400">Actions</div>
<div class="flex flex-nowrap items-center gap-2 overflow-x-auto">
@if (!str($application->status)->startsWith('exited'))
@if (!$application->destination->server->isSwarm())
<button type="button" class="button shrink-0"
@click="document.getElementById('application-mobile-deploy-trigger')?.click()">
<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">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
</path>
<path d="M7.05 11.038v-3.988"></path>
</svg>
Redeploy
</button>
@endif
@if ($application->build_pack !== 'dockercompose')
@if ($application->destination->server->isSwarm())
<button type="button" class="button shrink-0"
@click="document.getElementById('application-mobile-deploy-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">
<path
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Update Service
</button>
@else
<button type="button" class="button shrink-0"
@click="document.getElementById('application-mobile-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">
<path
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart
</button>
@endif
@endif
<x-forms.button isError class="shrink-0"
@click="document.getElementById('application-mobile-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">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
Stop
</x-forms.button>
@else
<button type="button" class="button shrink-0"
@click="document.getElementById('application-mobile-deploy-trigger')?.click()">
<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">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</button>
@endif
@if (!$application->destination->server->isSwarm())
@if ($application->status === 'running')
<button type="button" class="button shrink-0"
@click="document.getElementById('application-mobile-force-deploy-trigger')?.click()">
<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">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Force deploy (without cache)
</button>
@else
<button type="button" class="button shrink-0"
@click="document.getElementById('application-mobile-deploy-force-trigger')?.click()">
<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">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Force deploy (without cache)
</button>
@endif
@endif
</div>
</div>
@endif
<label id="application-mobile-section-label" for="application-mobile-section" class="mb-1 block text-xs font-semibold uppercase tracking-wide text-neutral-500 dark:text-neutral-400">Section</label>
<select id="application-mobile-section" class="select w-full" aria-label="Application menu"
data-current-value="{{ $activeMobileMenuValue }}"
x-data="{
init() {
this.syncFromLocation();
window.Livewire?.hook?.('morphed', ({ el }) => {
if (el.contains(this.$el)) {
queueMicrotask(() => this.syncFromLocation());
}
});
},
selected: $el.dataset.currentValue,
current: $el.dataset.currentValue,
syncFromLocation() {
const currentUrl = new URL(window.location.href);
const matchingOptions = Array.from(this.$el.options).filter((option) => {
if (!option.value.startsWith('navigate|') && !option.value.startsWith('location|')) {
return false;
}
const optionUrl = new URL(option.value.split('|').slice(2).join('|'), window.location.origin);
return optionUrl.pathname === currentUrl.pathname;
});
const selectedOption = matchingOptions.find((option) => {
return option.value.startsWith('navigate|configuration|') || option.value.startsWith('navigate|resource|');
}) || matchingOptions[0];
if (selectedOption) {
this.current = selectedOption.value;
this.selected = selectedOption.value;
}
},
resetToCurrent() {
this.selected = this.current;
},
}"
x-on:livewire:navigated.window="syncFromLocation()"
x-model="selected"
x-on:change="{{ $mobileSectionChangeHandler }}">
<optgroup label="Application">
@foreach ($applicationMenuItems as $menuItem)
<option value="{{ ($menuItem['navigate'] ?? true) ? 'navigate' : 'location' }}|application|{{ route($menuItem['route'], $parameters) }}">
{{ $menuItem['label'] }}
</option>
@endforeach
</optgroup>
<optgroup label="Configuration">
@foreach ($configurationMenuItems as $menuItem)
<option value="navigate|configuration|{{ route($menuItem['route'], $parameters) }}">
{{ $menuItem['label'] }}
</option>
@endforeach
</optgroup>
<optgroup label="Links">
@if (
(data_get($application, 'fqdn') ||
collect(json_decode($application->docker_compose_domains))->contains(fn($fqdn) => !empty(data_get($fqdn, 'domain'))) ||
data_get($application, 'previews', collect([]))->count() > 0 ||
data_get($application, 'ports_mappings_array')) &&
data_get($application, 'settings.is_raw_compose_deployment_enabled') !== true)
@if (data_get($application, 'gitBrancLocation'))
<option value="external:{{ $application->gitBranchLocation }}">Git Repository</option>
@endif
@if (data_get($application, 'build_pack') === 'dockercompose')
@foreach (collect(json_decode($application->docker_compose_domains)) as $fqdn)
@if (data_get($fqdn, 'domain'))
@foreach (explode(',', data_get($fqdn, 'domain')) as $domain)
<option value="external:{{ getFqdnWithoutPort($domain) }}">{{ getFqdnWithoutPort($domain) }}</option>
@endforeach
@endif
@endforeach
@endif
@if (data_get($application, 'fqdn'))
@foreach (str(data_get($application, 'fqdn'))->explode(',') as $fqdn)
<option value="external:{{ getFqdnWithoutPort($fqdn) }}">{{ getFqdnWithoutPort($fqdn) }}</option>
@endforeach
@endif
@if (data_get($application, 'previews', collect())->count() > 0)
@if (data_get($application, 'build_pack') === 'dockercompose')
@foreach ($application->previews as $preview)
@foreach (collect(json_decode($preview->docker_compose_domains)) as $fqdn)
@if (data_get($fqdn, 'domain'))
@foreach (explode(',', data_get($fqdn, 'domain')) as $domain)
<option value="external:{{ getFqdnWithoutPort($domain) }}">PR{{ data_get($preview, 'pull_request_id') }} | {{ getFqdnWithoutPort($domain) }}</option>
@endforeach
@endif
@endforeach
@endforeach
@else
@foreach (data_get($application, 'previews') as $preview)
@if (data_get($preview, 'fqdn'))
<option value="external:{{ getFqdnWithoutPort(data_get($preview, 'fqdn')) }}">PR{{ data_get($preview, 'pull_request_id') }} | {{ data_get($preview, 'fqdn') }}</option>
@endif
@endforeach
@endif
@endif
@if (data_get($application, 'ports_mappings_array'))
@foreach ($application->ports_mappings_array as $port)
@if ($application->destination->server->id === 0)
<option value="external:http://localhost:{{ explode(':', $port)[0] }}">Port {{ $port }}</option>
@else
<option value="external:http://{{ $application->destination->server->ip }}:{{ explode(':', $port)[0] }}">{{ $application->destination->server->ip }}:{{ explode(':', $port)[0] }}</option>
@if (count($application->additional_servers) > 0)
@foreach ($application->additional_servers as $server)
<option value="external:http://{{ $server->ip }}:{{ explode(':', $port)[0] }}">{{ $server->ip }}:{{ explode(':', $port)[0] }}</option>
@endforeach
@endif
@endif
@endforeach
@endif
@else
<option disabled>No links available</option>
@endif
</optgroup>
</select>
<x-modal-confirmation 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.',
]" :confirmWithText="false" :confirmWithPassword="false"
step1ButtonText="Continue" step2ButtonText="Confirm">
<x-slot:trigger>
<button id="application-mobile-stop-trigger" type="button" class="hidden">Stop</button>
</x-slot:trigger>
</x-modal-confirmation>
<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:trigger>
<button id="application-mobile-deploy-trigger" type="button" class="hidden">Deploy</button>
</x-slot:trigger>
</x-modal-confirmation>
<x-modal-confirmation title="Confirm Application Restart?" buttonTitle="Restart"
submitAction="restart" :actions="[
'This application will be restarted without rebuilding.',
]" :confirmWithText="false" :confirmWithPassword="false"
step2ButtonText="Confirm">
<x-slot:trigger>
<button id="application-mobile-restart-trigger" type="button" class="hidden">Restart</button>
</x-slot:trigger>
</x-modal-confirmation>
<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:trigger>
<button id="application-mobile-force-deploy-trigger" type="button" class="hidden">Force deploy</button>
</x-slot:trigger>
</x-modal-confirmation>
<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:trigger>
<button id="application-mobile-deploy-force-trigger" type="button" class="hidden">Force deploy</button>
</x-slot:trigger>
</x-modal-confirmation>
</div>
<nav
class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-scroll overflow-y-hidden pb-1 whitespace-nowrap md:w-auto md:overflow-visible">
<a class="shrink-0 {{ request()->routeIs('project.application.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
class="scrollbar hidden min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-scroll overflow-y-hidden pb-1 whitespace-nowrap md:flex md:w-auto md:overflow-visible">
<a class="hidden md:block shrink-0 {{ request()->routeIs('project.application.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('project.application.configuration', $parameters) }}">
Configuration
</a>
<a class="shrink-0 {{ request()->routeIs('project.application.deployment.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
<a class="hidden md:block shrink-0 {{ request()->routeIs('project.application.deployment.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('project.application.deployment.index', $parameters) }}">
Deployments
</a>
<a class="shrink-0 {{ request()->routeIs('project.application.logs') ? 'dark:text-white' : '' }}"
<a class="hidden md:block shrink-0 {{ request()->routeIs('project.application.logs') ? 'dark:text-white' : '' }}"
href="{{ route('project.application.logs', $parameters) }}">
<div class="flex items-center gap-1">
Logs
@@ -24,13 +460,13 @@
</a>
@if (!$application->destination->server->isSwarm())
@can('canAccessTerminal')
<a class="shrink-0 {{ request()->routeIs('project.application.command') ? 'dark:text-white' : '' }}"
<a class="hidden md:block shrink-0 {{ request()->routeIs('project.application.command') ? 'dark:text-white' : '' }}"
href="{{ route('project.application.command', $parameters) }}">
Terminal
</a>
@endcan
@endif
<div class="shrink-0">
<div class="hidden shrink-0 md:block">
<x-applications.links :application="$application" />
</div>
</nav>
@@ -38,62 +474,6 @@
@if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
<div>Please load a Compose file.</div>
@else
<div class="md:hidden">
<x-dropdown>
<x-slot:title>
Actions
</x-slot>
@if (!str($application->status)->startsWith('exited'))
@if (!$application->destination->server->isSwarm())
<div class="dropdown-item dropdown-item-touch" wire:click='deploy'>
Redeploy
</div>
@endif
@if ($application->build_pack !== 'dockercompose')
@if ($application->destination->server->isSwarm())
<div class="dropdown-item dropdown-item-touch" wire:click='deploy'>
Update Service
</div>
@else
<div class="dropdown-item dropdown-item-touch" wire:click='restart'>
Restart
</div>
@endif
@endif
<x-modal-confirmation 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.',
]" :confirmWithText="false" :confirmWithPassword="false"
step1ButtonText="Continue" step2ButtonText="Confirm">
<x-slot:trigger>
<div class="dropdown-item dropdown-item-touch text-error">
Stop
</div>
</x-slot:trigger>
</x-modal-confirmation>
@else
<div class="dropdown-item dropdown-item-touch" wire:click='deploy'>
Deploy
</div>
@endif
@if (!$application->destination->server->isSwarm())
<div class="mx-2 my-1 border-t border-neutral-200 dark:border-coolgray-300"></div>
@if ($application->status === 'running')
<div class="dropdown-item dropdown-item-touch" wire:click='force_deploy_without_cache'>
Force deploy (without cache)
</div>
@else
<div class="dropdown-item dropdown-item-touch" wire:click='deploy(true)'>
Force deploy (without cache)
</div>
@endif
@endif
</x-dropdown>
</div>
<div class="hidden flex-wrap items-center gap-2 md:flex">
@if (!$application->destination->server->isSwarm())
<div>
@@ -103,49 +483,75 @@
<div class="flex flex-wrap gap-2">
@if (!str($application->status)->startsWith('exited'))
@if (!$application->destination->server->isSwarm())
<x-forms.button title="With rolling update if possible" wire:click='deploy'>
<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">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
</path>
<path d="M7.05 11.038v-3.988"></path>
</svg>
Redeploy
</x-forms.button>
<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 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">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
</path>
<path d="M7.05 11.038v-3.988"></path>
</svg>
Redeploy
</x-forms.button>
</x-slot:content>
</x-modal-confirmation>
@endif
@if ($application->build_pack !== 'dockercompose')
@if ($application->destination->server->isSwarm())
<x-forms.button title="Redeploy Swarm Service (rolling update)" wire:click='deploy'>
<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">
<path
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Update Service
</x-forms.button>
<x-modal-confirmation title="Confirm Application Deployment?" buttonTitle="Update Service"
submitAction="deploy" :actions="[
'This Swarm service will be updated with a rolling deployment.',
]" :confirmWithText="false" :confirmWithPassword="false"
step2ButtonText="Confirm">
<x-slot:content>
<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"
stroke-linejoin="round" stroke-width="2">
<path
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Update Service
</x-forms.button>
</x-slot:content>
</x-modal-confirmation>
@else
<x-forms.button title="Restart without rebuilding" wire:click='restart'>
<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">
<path
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart
</x-forms.button>
<x-modal-confirmation title="Confirm Application Restart?" buttonTitle="Restart"
submitAction="restart" :actions="[
'This application will be restarted without rebuilding.',
]" :confirmWithText="false" :confirmWithPassword="false"
step2ButtonText="Confirm">
<x-slot:content>
<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"
stroke-linejoin="round" stroke-width="2">
<path
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart
</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.',
@@ -167,15 +573,25 @@
</x-slot:button-title>
</x-modal-confirmation>
@else
<x-forms.button wire:click='deploy'>
<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">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</x-forms.button>
<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 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">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</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
@@ -5,8 +5,8 @@
<h1>Configuration</h1>
<livewire:project.shared.configuration-checker :resource="$database" />
<livewire:project.database.heading :database="$database" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<div class="sub-menu-wrapper">
<div class="flex flex-col h-full gap-4 md:gap-8 md:flex-row">
<div class="sub-menu-wrapper hidden md:flex">
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.database.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}"><span class="menu-item-label">General</span></a>
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
@@ -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">
@@ -1,16 +1,190 @@
<nav wire:poll.10000ms="checkStatus" class="pb-6">
@php
$databasePageItems = [
['label' => 'Configuration', 'route' => 'project.database.configuration', 'active' => request()->routeIs('project.database.configuration')],
['label' => 'Logs', 'route' => 'project.database.logs', 'active' => request()->routeIs('project.database.logs')],
['label' => 'Terminal', 'route' => 'project.database.command', 'active' => request()->routeIs('project.database.command'), 'navigate' => false, 'visible' => auth()->user()?->can('canAccessTerminal')],
[
'label' => 'Backups',
'route' => 'project.database.backup.index',
'active' => request()->routeIs('project.database.backup.index', 'project.database.backup.execution'),
'visible' => in_array($database->getMorphClass(), [
'App\Models\StandalonePostgresql',
'App\Models\StandaloneMongodb',
'App\Models\StandaloneMysql',
'App\Models\StandaloneMariadb',
]),
],
];
$databaseConfigurationItems = [
['label' => 'General', 'route' => 'project.database.configuration', 'active' => request()->routeIs('project.database.configuration')],
['label' => 'Environment Variables', 'route' => 'project.database.environment-variables', 'active' => request()->routeIs('project.database.environment-variables')],
['label' => 'Servers', 'route' => 'project.database.servers', 'active' => request()->routeIs('project.database.servers')],
['label' => 'Persistent Storage', 'route' => 'project.database.persistent-storage', 'active' => request()->routeIs('project.database.persistent-storage')],
['label' => 'Import Backup', 'route' => 'project.database.import-backup', 'active' => request()->routeIs('project.database.import-backup'), 'visible' => auth()->user()?->can('update', $database)],
['label' => 'Webhooks', 'route' => 'project.database.webhooks', 'active' => request()->routeIs('project.database.webhooks')],
['label' => 'Healthcheck', 'route' => 'project.database.healthcheck', 'active' => request()->routeIs('project.database.healthcheck')],
['label' => 'Resource Limits', 'route' => 'project.database.resource-limits', 'active' => request()->routeIs('project.database.resource-limits')],
['label' => 'Resource Operations', 'route' => 'project.database.resource-operations', 'active' => request()->routeIs('project.database.resource-operations')],
['label' => 'Metrics', 'route' => 'project.database.metrics', 'active' => request()->routeIs('project.database.metrics')],
['label' => 'Tags', 'route' => 'project.database.tags', 'active' => request()->routeIs('project.database.tags')],
['label' => 'Danger Zone', 'route' => 'project.database.danger', 'active' => request()->routeIs('project.database.danger')],
];
$databasePageItems = array_values(array_filter($databasePageItems, fn (array $item): bool => $item['visible'] ?? true));
$databaseConfigurationItems = array_values(array_filter($databaseConfigurationItems, fn (array $item): bool => $item['visible'] ?? true));
$activeDatabaseConfigurationItem = collect($databaseConfigurationItems)->firstWhere('active', true);
$activeDatabasePageItem = collect($databasePageItems)->firstWhere('active', true);
$activeDatabaseMobileItem = $activeDatabaseConfigurationItem ?? $activeDatabasePageItem ?? $databasePageItems[0];
$activeDatabaseMobileGroup = $activeDatabaseConfigurationItem ? 'configuration' : 'database';
$activeDatabaseMobileNavigation = ($activeDatabaseMobileItem['navigate'] ?? true) ? 'navigate' : 'location';
$activeDatabaseMobileValue = $activeDatabaseMobileNavigation.'|'.$activeDatabaseMobileGroup.'|'.route($activeDatabaseMobileItem['route'], $parameters);
$databaseMobileMenuChangeHandler = <<<'JS'
const value = $event.target.value;
if (!value) {
return;
}
if (value.startsWith('navigate|')) {
const url = value.split('|').slice(2).join('|');
window.Livewire?.navigate ? window.Livewire.navigate(url) : window.location.href = url;
return;
}
if (value.startsWith('location|')) {
const url = value.split('|').slice(2).join('|');
window.location.href = url;
return;
}
resetToCurrent();
if (value.startsWith('external:')) {
window.open(value.slice(9), '_blank', 'noopener');
return;
}
document.getElementById(`database-${value.slice(7)}-trigger`)?.click();
JS;
@endphp
<x-resources.breadcrumbs :resource="$database" :parameters="$parameters" />
<x-slide-over @startdatabase.window="slideOverOpen = true" closeWithX fullScreen>
<x-slot:title>Database Startup</x-slot:title>
<x-slot:content>
<div wire:ignore>
<div wire:ignore class="h-full min-h-0 min-w-0 max-w-full">
<livewire:activity-monitor header="Logs" fullHeight />
</div>
</x-slot:content>
</x-slide-over>
<div class="navbar-main">
<div class="w-full md:hidden">
@if ($database->destination->server->isFunctional())
<div id="database-mobile-actions" class="mt-2 mb-3 md:hidden">
<div class="mb-1 text-xs font-semibold uppercase tracking-wide text-neutral-500 dark:text-neutral-400">Actions</div>
<div class="flex flex-nowrap items-center gap-2 overflow-x-auto">
@if (!str($database->status)->startsWith('exited'))
<button type="button" class="button shrink-0"
@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"
stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart
</button>
<x-forms.button isError class="shrink-0"
@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">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
Stop
</x-forms.button>
@else
<button type="button" class="button shrink-0"
@click="document.getElementById('database-start-trigger')?.click()">
<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">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Start
</button>
@endif
</div>
</div>
@endif
<label id="database-mobile-section-label" for="database-mobile-section" class="mb-1 block text-xs font-semibold uppercase tracking-wide text-neutral-500 dark:text-neutral-400">Section</label>
<select id="database-mobile-section" class="select w-full" aria-label="Database menu"
data-current-value="{{ $activeDatabaseMobileValue }}"
x-data="{
init() {
this.syncFromLocation();
window.Livewire?.hook?.('morphed', ({ el }) => {
if (el.contains(this.$el)) {
queueMicrotask(() => this.syncFromLocation());
}
});
},
selected: $el.dataset.currentValue,
current: $el.dataset.currentValue,
syncFromLocation() {
const currentUrl = new URL(window.location.href);
const matchingOptions = Array.from(this.$el.options).filter((option) => {
if (!option.value.startsWith('navigate|') && !option.value.startsWith('location|')) {
return false;
}
const optionUrl = new URL(option.value.split('|').slice(2).join('|'), window.location.origin);
return optionUrl.pathname === currentUrl.pathname;
});
const selectedOption = matchingOptions.find((option) => {
return option.value.startsWith('navigate|configuration|') || option.value.startsWith('navigate|resource|');
}) || matchingOptions[0];
if (selectedOption) {
this.current = selectedOption.value;
this.selected = selectedOption.value;
}
},
resetToCurrent() {
this.selected = this.current;
},
}"
x-on:livewire:navigated.window="syncFromLocation()"
x-model="selected"
x-on:change="{{ $databaseMobileMenuChangeHandler }}">
<optgroup label="Database">
@foreach ($databasePageItems as $menuItem)
<option value="{{ ($menuItem['navigate'] ?? true) ? 'navigate' : 'location' }}|database|{{ route($menuItem['route'], $parameters) }}">
{{ $menuItem['label'] }}
</option>
@endforeach
</optgroup>
<optgroup label="Configuration">
@foreach ($databaseConfigurationItems as $menuItem)
<option value="navigate|configuration|{{ route($menuItem['route'], $parameters) }}">
{{ $menuItem['label'] }}
</option>
@endforeach
</optgroup>
</select>
</div>
<nav
class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-scroll overflow-y-hidden pb-1 whitespace-nowrap md:w-auto md:overflow-visible">
class="scrollbar hidden min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-scroll overflow-y-hidden pb-1 whitespace-nowrap md:flex md:w-auto md:overflow-visible">
<a class="shrink-0 {{ request()->routeIs('project.database.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('project.database.configuration', $parameters) }}">
Configuration
@@ -37,89 +211,39 @@
</a>
@endif
</nav>
@if ($database->destination->server->isFunctional())
<div class="flex flex-wrap gap-2 items-center">
<div class="md:hidden">
<x-dropdown>
<x-slot:title>
Actions
</x-slot>
@if (!str($database->status)->startsWith('exited'))
<x-modal-confirmation title="Confirm Database Restart?" buttonTitle="Restart" submitAction="restart"
:actions="[
'This database will be unavailable during the restart.',
'If the database is currently in use data could be lost.',
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Restart Database"
:dispatchEvent="true" dispatchEventType="restartEvent">
<x-slot:trigger>
<div class="dropdown-item dropdown-item-touch">
Restart
</div>
</x-slot:trigger>
</x-modal-confirmation>
<x-modal-confirmation title="Confirm Database Stopping?" buttonTitle="Stop" submitAction="stop"
:checkboxes="$checkboxes" :actions="[
'This database will be stopped.',
'If the database is currently in use data could be lost.',
'All non-persistent data of this database (containers, networks, unused images) will be deleted (don\'t worry, no data is lost and you can start the database again).',
]" :confirmWithText="false" :confirmWithPassword="false"
step1ButtonText="Continue" step2ButtonText="Confirm">
<x-slot:trigger>
<div class="dropdown-item dropdown-item-touch text-error">
Stop
</div>
</x-slot:trigger>
</x-modal-confirmation>
@else
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('startEvent')">
Start
</div>
@endif
</x-dropdown>
</div>
<div class="hidden flex-wrap items-center gap-2 md:flex">
@if (!str($database->status)->startsWith('exited'))
<x-modal-confirmation title="Confirm Database Restart?" buttonTitle="Restart" submitAction="restart"
:actions="[
'This database will be unavailable during the restart.',
'If the database is currently in use data could be lost.',
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Restart Database"
:dispatchEvent="true" dispatchEventType="restartEvent">
<x-slot:button-title>
<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">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart
</x-slot:button-title>
</x-modal-confirmation>
<x-modal-confirmation title="Confirm Database Stopping?" buttonTitle="Stop" submitAction="stop"
:checkboxes="$checkboxes" :actions="[
'This database will be stopped.',
'If the database is currently in use data could be lost.',
'All non-persistent data of this database (containers, networks, unused images) will be deleted (don\'t worry, no data is lost and you can start the database again).',
]" :confirmWithText="false" :confirmWithPassword="false"
step1ButtonText="Continue" step2ButtonText="Confirm">
<x-slot:button-title>
<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">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
Stop
</x-slot:button-title>
</x-modal-confirmation>
<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"
stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart
</x-forms.button>
<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">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
Stop
</x-forms.button>
@else
<button @click="$wire.dispatch('startEvent')" 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">
@@ -127,7 +251,7 @@
<path d="M7 4v16l13 -8z" />
</svg>
Start
</button>
</x-forms.button>
@endif
</div>
@script
@@ -148,4 +272,37 @@
<div class="text-error">Underlying server is not functional.</div>
@endif
</div>
@if ($database->destination->server->isFunctional())
<x-modal-confirmation title="Confirm Database Restart?" buttonTitle="Restart" submitAction="restartEvent"
:actions="[
'This database will be unavailable during the restart.',
'If the database is currently in use data could be lost.',
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Restart Database"
:dispatchAction="true">
<x-slot:trigger>
<button id="database-restart-trigger" type="button" class="hidden">Restart</button>
</x-slot:trigger>
</x-modal-confirmation>
<x-modal-confirmation title="Confirm Database Stopping?" buttonTitle="Stop" submitAction="stop"
:checkboxes="$checkboxes" :actions="[
'This database will be stopped.',
'If the database is currently in use data could be lost.',
'All non-persistent data of this database (containers, networks, unused images) will be deleted (don\'t worry, no data is lost and you can start the database again).',
]" :confirmWithText="false" :confirmWithPassword="false"
step1ButtonText="Continue" step2ButtonText="Confirm">
<x-slot:trigger>
<button id="database-stop-trigger" type="button" class="hidden">Stop</button>
</x-slot:trigger>
</x-modal-confirmation>
<x-modal-confirmation title="Confirm Database Start?" buttonTitle="Start" submitAction="startEvent"
:actions="[
'This database will be started.',
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Start Database"
:dispatchAction="true">
<x-slot:trigger>
<button id="database-start-trigger" type="button" class="hidden">Start</button>
</x-slot:trigger>
</x-modal-confirmation>
@endif
</nav>
@@ -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>
@@ -11,38 +11,55 @@
@endcan
</div>
<div class="subtitle">All your projects are here.</div>
<div class="grid grid-cols-1 gap-4 xl:grid-cols-2 -mt-1">
@foreach ($projects as $project)
<div class="relative gap-2 cursor-pointer coolbox group">
<a href="{{ $project->navigateTo() }}" {{ wireNavigate() }} class="absolute inset-0"></a>
<div class="flex flex-1 mx-6">
<div class="flex flex-col justify-center flex-1">
<div class="box-title">{{ $project->name }}</div>
<div class="box-description">
{{ $project->description }}
@if ($projects->count() > 0)
<div class="grid grid-cols-1 gap-4 xl:grid-cols-2 -mt-1">
@foreach ($projects as $project)
<div class="relative gap-2 cursor-pointer coolbox group">
<a href="{{ $project->navigateTo() }}" {{ wireNavigate() }} class="absolute inset-0"></a>
<div class="flex flex-1 mx-6">
<div class="flex flex-col justify-center flex-1">
<div class="box-title">{{ $project->name }}</div>
<div class="box-description">
{{ $project->description }}
</div>
</div>
</div>
<div class="relative z-10 flex items-center justify-center gap-4 text-xs font-bold">
@if ($project->environments->first())
@can('createAnyResource')
<div class="relative z-10 flex items-center justify-center gap-4 text-xs font-bold">
@if ($project->environments->first())
@can('createAnyResource')
<a class="hover:underline" {{ wireNavigate() }}
href="{{ route('project.resource.create', [
'project_uuid' => $project->uuid,
'environment_uuid' => $project->environments->first()->uuid,
]) }}">
+ Add Resource
</a>
@endcan
@endif
@can('update', $project)
<a class="hover:underline" {{ wireNavigate() }}
href="{{ route('project.resource.create', [
'project_uuid' => $project->uuid,
'environment_uuid' => $project->environments->first()->uuid,
]) }}">
+ Add Resource
href="{{ route('project.edit', ['project_uuid' => $project->uuid]) }}">
Settings
</a>
@endcan
@endif
@can('update', $project)
<a class="hover:underline" {{ wireNavigate() }}
href="{{ route('project.edit', ['project_uuid' => $project->uuid]) }}">
Settings
</a>
@endcan
</div>
</div>
</div>
@endforeach
</div>
@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">
@can('createAnyResource')
<x-modal-input buttonTitle="Add" title="New Project">
<livewire:project.add-empty />
</x-modal-input> your first project or
@else
Create your first project or
@endcan
go to the <a class="underline dark:text-white" href="{{ route('onboarding') }}"
{{ wireNavigate() }}>onboarding</a> page.
</div>
@endforeach
</div>
</div>
@endif
</div>
@@ -138,9 +138,14 @@
</div>
</div>
<div x-show="filteredServices.length > 0" class="mt-8">
<div class="flex items-center gap-4" x-init="loadResources">
<div class="flex flex-wrap items-center gap-4" x-init="loadResources">
<h2>Services</h2>
<x-forms.button x-on:click="loadResources">Reload List</x-forms.button>
<div x-show="serviceTemplatesLastUpdated"
class="text-xs text-neutral-500 dark:text-neutral-400">
Last Updated on Service Templates:
<span x-text="serviceTemplatesLastUpdated"></span>
</div>
</div>
<x-callout type="info" title="Trademarks Policy" class="mt-4 mb-6">
The respective trademarks mentioned here are owned by the respective companies, and use of them
@@ -149,12 +154,19 @@
<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>
<template x-if="service.name">
<span x-text="service.name"></span>
<div>
<span x-text="service.name"></span>
<template x-if="service.templateLastUpdated">
<div class="mt-1 text-[0.7rem] font-normal text-neutral-500 dark:text-neutral-500">
Updated: <span x-text="service.templateLastUpdated"></span>
</div>
</template>
</div>
</template>
</x-slot>
<x-slot:description>
@@ -202,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] }"
@@ -237,6 +249,7 @@
isSticky: false,
selecting: false,
services: [],
serviceTemplatesLastUpdated: null,
gitBasedApplications: [],
dockerBasedApplications: [],
databases: [],
@@ -251,12 +264,14 @@
this.loading = true;
const {
services,
serviceTemplatesLastUpdated,
categories,
gitBasedApplications,
dockerBasedApplications,
databases
} = await this.$wire.loadServices();
this.services = services;
this.serviceTemplatesLastUpdated = serviceTemplatesLastUpdated;
this.categories = categories || [];
this.gitBasedApplications = gitBasedApplications;
this.dockerBasedApplications = dockerBasedApplications;
@@ -272,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) {
@@ -307,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,8 +4,8 @@
</x-slot>
<livewire:project.service.heading :service="$service" :parameters="$parameters" :query="$query" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<div class="sub-menu-wrapper">
<div class="flex flex-col h-full gap-4 md:gap-8 md:flex-row">
<div class="sub-menu-wrapper hidden md:flex">
<a class="sub-menu-item" target="_blank" href="{{ $service->documentation() }}"><span class="menu-item-label">Documentation</span>
<x-external-link /></a>
<a class='sub-menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
@@ -1,6 +1,6 @@
<div>
<livewire:project.service.heading :service="$service" :parameters="$parameters" :query="$query" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<div class="flex flex-col h-full gap-4 md:gap-8 md:flex-row">
<x-service-database.sidebar :parameters="$parameters" :serviceDatabase="$serviceDatabase" :isImportSupported="$isImportSupported" />
<div class="w-full">
<x-slot:title>
@@ -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>
@@ -1,4 +1,94 @@
<div wire:poll.10000ms="checkStatus" class="pb-6">
@php
$servicePageItems = [
['label' => 'Configuration', 'route' => 'project.service.configuration', 'active' => request()->routeIs('project.service.configuration')],
['label' => 'Logs', 'route' => 'project.service.logs', 'active' => request()->routeIs('project.service.logs')],
['label' => 'Terminal', 'route' => 'project.service.command', 'active' => request()->routeIs('project.service.command'), 'navigate' => false, 'visible' => auth()->user()?->can('canAccessTerminal')],
];
$serviceConfigurationItems = [
['label' => 'General', 'route' => 'project.service.configuration', 'active' => request()->routeIs('project.service.configuration')],
['label' => 'Environment Variables', 'route' => 'project.service.environment-variables', 'active' => request()->routeIs('project.service.environment-variables')],
['label' => 'Persistent Storages', 'route' => 'project.service.storages', 'active' => request()->routeIs('project.service.storages')],
['label' => 'Scheduled Tasks', 'route' => 'project.service.scheduled-tasks.show', 'active' => request()->routeIs('project.service.scheduled-tasks.show', 'project.service.scheduled-tasks')],
['label' => 'Webhooks', 'route' => 'project.service.webhooks', 'active' => request()->routeIs('project.service.webhooks')],
['label' => 'Resource Operations', 'route' => 'project.service.resource-operations', 'active' => request()->routeIs('project.service.resource-operations')],
['label' => 'Tags', 'route' => 'project.service.tags', 'active' => request()->routeIs('project.service.tags')],
['label' => 'Danger Zone', 'route' => 'project.service.danger', 'active' => request()->routeIs('project.service.danger')],
];
$serviceResourceItems = [];
if (filled(data_get($parameters, 'stack_service_uuid'))) {
$serviceResourceItems = [
['label' => 'Back', 'route' => 'project.service.configuration', 'active' => false, 'parameters' => [...$parameters, 'stack_service_uuid' => null]],
['label' => 'General', 'route' => 'project.service.index', 'active' => request()->routeIs('project.service.index')],
['label' => 'Advanced', 'route' => 'project.service.index.advanced', 'active' => request()->routeIs('project.service.index.advanced')],
['label' => 'Backups', 'route' => 'project.service.database.backups', 'active' => request()->routeIs('project.service.database.backups')],
['label' => 'Import Backup', 'route' => 'project.service.database.import', 'active' => request()->routeIs('project.service.database.import')],
];
}
$servicePageItems = array_values(array_filter($servicePageItems, fn (array $item): bool => $item['visible'] ?? true));
$serviceResourceItems = array_values(array_filter($serviceResourceItems, fn (array $item): bool => $item['visible'] ?? true));
$activeServiceResourceItem = collect($serviceResourceItems)->firstWhere('active', true);
$activeServiceConfigurationItem = collect($serviceConfigurationItems)->firstWhere('active', true);
$activeServicePageItem = collect($servicePageItems)->firstWhere('active', true);
$activeServiceMobileItem = $activeServiceResourceItem ?? $activeServiceConfigurationItem ?? $activeServicePageItem ?? $servicePageItems[0];
$activeServiceMobileGroup = $activeServiceResourceItem ? 'resource' : ($activeServiceConfigurationItem ? 'configuration' : 'service');
$activeServiceMobileNavigation = ($activeServiceMobileItem['navigate'] ?? true) ? 'navigate' : 'location';
$activeServiceMobileValue = $activeServiceMobileNavigation.'|'.$activeServiceMobileGroup.'|'.route($activeServiceMobileItem['route'], $activeServiceMobileItem['parameters'] ?? $parameters);
$serviceLinks = collect([]);
$service->applications()->get()->each(function ($application) use ($serviceLinks) {
$type = $application->serviceType();
if ($type) {
generateServiceSpecificFqdns($application)
->map(fn ($link) => getFqdnWithoutPort($link))
->each(fn ($link) => $serviceLinks->push($link));
} else {
if ($application->fqdn) {
collect(str($application->fqdn)->explode(','))
->each(fn ($fqdn) => $serviceLinks->push(getFqdnWithoutPort($fqdn)));
}
if ($application->ports) {
collect(str($application->ports)->explode(','))
->each(function ($port) use ($serviceLinks) {
$hostPort = str($port)->contains(':') ? str($port)->before(':') : $port;
$serviceLinks->push(base_url(withPort: false).":{$hostPort}");
});
}
}
});
$serviceMobileMenuChangeHandler = <<<'JS'
const value = $event.target.value;
if (!value) {
return;
}
if (value.startsWith('navigate|')) {
const url = value.split('|').slice(2).join('|');
window.Livewire?.navigate ? window.Livewire.navigate(url) : window.location.href = url;
return;
}
if (value.startsWith('location|')) {
const url = value.split('|').slice(2).join('|');
window.location.href = url;
return;
}
resetToCurrent();
if (value.startsWith('external:')) {
window.open(value.slice(9), '_blank', 'noopener');
return;
}
const action = value.slice(7);
document.getElementById(`service-${action}-trigger`)?.click();
JS;
@endphp
<livewire:project.shared.configuration-checker :resource="$service" />
<x-slide-over @startservice.window="slideOverOpen = true" closeWithX fullScreen>
<x-slot:title>Service Startup</x-slot:title>
@@ -9,8 +99,252 @@
<h1>{{ $title }}</h1>
<x-resources.breadcrumbs :resource="$service" :parameters="$parameters" />
<div class="navbar-main" x-data">
<div class="mb-4 w-full md:mb-0 md:hidden">
@if ($service->isDeployable)
<div id="service-mobile-actions" class="mt-2 mb-3 md:hidden">
<div class="mb-1 text-xs font-semibold uppercase tracking-wide text-neutral-500 dark:text-neutral-400">Actions</div>
<div class="flex flex-nowrap items-center gap-2 overflow-x-auto">
@if (str($service->status)->contains('running'))
<button type="button" class="button shrink-0"
@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">
<path d="M19.933 13.041 a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart
</button>
<x-forms.button isError class="shrink-0"
@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">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
Stop
</x-forms.button>
<button type="button" class="button shrink-0"
@click="document.getElementById('service-pullAndRestart-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">
<path d="M19.933 13.041 a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Pull Latest Images & Restart
</button>
@elseif (str($service->status)->contains('degraded'))
<button type="button" class="button shrink-0"
@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">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart
</button>
<x-forms.button isError class="shrink-0"
@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">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
Stop
</x-forms.button>
<button type="button" class="button shrink-0"
@click="document.getElementById('service-forceDeploy-trigger')?.click()">
<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">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Force Restart
</button>
@elseif (str($service->status)->contains('exited'))
<button type="button" class="button shrink-0"
@click="document.getElementById('service-start-trigger')?.click()">
<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">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</button>
<button type="button" class="button shrink-0"
@click="document.getElementById('service-forceDeploy-trigger')?.click()">
<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">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Force Deploy
</button>
<button type="button" class="button shrink-0"
@click="document.getElementById('service-cleanup-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">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
Force Cleanup Containers
</button>
@else
<x-forms.button isError class="shrink-0"
@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">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
Stop
</x-forms.button>
<button type="button" class="button shrink-0"
@click="document.getElementById('service-start-trigger')?.click()">
<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">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</button>
<button type="button" class="button shrink-0"
@click="document.getElementById('service-forceDeploy-trigger')?.click()">
<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">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Force Deploy
</button>
<button type="button" class="button shrink-0"
@click="document.getElementById('service-cleanup-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">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
Force Cleanup Containers
</button>
@endif
</div>
</div>
@endif
<label id="service-mobile-section-label" for="service-mobile-section" class="mb-1 block text-xs font-semibold uppercase tracking-wide text-neutral-500 dark:text-neutral-400">Section</label>
<select id="service-mobile-section" class="select w-full" aria-label="Service menu"
data-current-value="{{ $activeServiceMobileValue }}"
x-data="{
init() {
this.syncFromLocation();
window.Livewire?.hook?.('morphed', ({ el }) => {
if (el.contains(this.$el)) {
queueMicrotask(() => this.syncFromLocation());
}
});
},
selected: $el.dataset.currentValue,
current: $el.dataset.currentValue,
syncFromLocation() {
const currentUrl = new URL(window.location.href);
const matchingOptions = Array.from(this.$el.options).filter((option) => {
if (!option.value.startsWith('navigate|') && !option.value.startsWith('location|')) {
return false;
}
const optionUrl = new URL(option.value.split('|').slice(2).join('|'), window.location.origin);
return optionUrl.pathname === currentUrl.pathname;
});
const selectedOption = matchingOptions.find((option) => {
return option.value.startsWith('navigate|configuration|') || option.value.startsWith('navigate|resource|');
}) || matchingOptions[0];
if (selectedOption) {
this.current = selectedOption.value;
this.selected = selectedOption.value;
}
},
resetToCurrent() {
this.selected = this.current;
},
}"
x-on:livewire:navigated.window="syncFromLocation()"
x-model="selected"
x-on:change="{{ $serviceMobileMenuChangeHandler }}">
<optgroup label="Service">
@foreach ($servicePageItems as $menuItem)
<option value="{{ ($menuItem['navigate'] ?? true) ? 'navigate' : 'location' }}|service|{{ route($menuItem['route'], $parameters) }}">
{{ $menuItem['label'] }}
</option>
@endforeach
</optgroup>
<optgroup label="Configuration">
@foreach ($serviceConfigurationItems as $menuItem)
<option value="navigate|configuration|{{ route($menuItem['route'], $parameters) }}">
{{ $menuItem['label'] }}
</option>
@endforeach
</optgroup>
@if (count($serviceResourceItems) > 0)
<optgroup label="Resource">
@foreach ($serviceResourceItems as $menuItem)
<option value="navigate|resource|{{ route($menuItem['route'], $menuItem['parameters'] ?? $parameters) }}">
{{ $menuItem['label'] }}
</option>
@endforeach
</optgroup>
@endif
<optgroup label="Links">
@if (filled($service->documentation()))
<option value="external:{{ $service->documentation() }}">Documentation</option>
@endif
@forelse ($serviceLinks as $link)
<option value="external:{{ $link }}">{{ $link }}</option>
@empty
@if (blank($service->documentation()))
<option disabled>No links available</option>
@endif
@endforelse
</optgroup>
</select>
</div>
<nav
class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-scroll overflow-y-hidden pb-1 whitespace-nowrap md:w-auto md:overflow-visible">
class="scrollbar hidden min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-scroll overflow-y-hidden pb-1 whitespace-nowrap md:flex md:w-auto md:overflow-visible">
<a class="shrink-0 {{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
href="{{ route('project.service.configuration', $parameters) }}">
<button>Configuration</button>
@@ -29,85 +363,15 @@
<x-services.links :service="$service" />
</div>
</nav>
@if ($service->isDeployable)
<div class="order-first flex flex-wrap items-center gap-2 sm:order-last">
<div class="md:hidden">
<x-dropdown>
<x-slot:title>
Actions
</x-slot>
@if (str($service->status)->contains('running'))
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('restartEvent')">
Restart
</div>
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
<x-slot:trigger>
<div class="dropdown-item dropdown-item-touch text-error">
Stop
</div>
</x-slot:trigger>
</x-modal-confirmation>
<div class="mx-2 my-1 border-t border-neutral-200 dark:border-coolgray-300"></div>
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('pullAndRestartEvent')">
Pull Latest Images & Restart
</div>
@elseif (str($service->status)->contains('degraded'))
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('restartEvent')">
Restart
</div>
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
<x-slot:trigger>
<div class="dropdown-item dropdown-item-touch text-error">
Stop
</div>
</x-slot:trigger>
</x-modal-confirmation>
<div class="mx-2 my-1 border-t border-neutral-200 dark:border-coolgray-300"></div>
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('forceDeployEvent')">
Force Restart
</div>
@elseif (str($service->status)->contains('exited'))
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('startEvent')">
Deploy
</div>
<div class="mx-2 my-1 border-t border-neutral-200 dark:border-coolgray-300"></div>
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('forceDeployEvent')">
Force Deploy
</div>
<div class="dropdown-item dropdown-item-touch" wire:click='stop(true)'>
Force Cleanup Containers
</div>
@else
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
<x-slot:trigger>
<div class="dropdown-item dropdown-item-touch text-error">
Stop
</div>
</x-slot:trigger>
</x-modal-confirmation>
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('startEvent')">
Deploy
</div>
<div class="mx-2 my-1 border-t border-neutral-200 dark:border-coolgray-300"></div>
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('forceDeployEvent')">
Force Deploy
</div>
<div class="dropdown-item dropdown-item-touch" wire:click='stop(true)'>
Force Cleanup Containers
</div>
@endif
</x-dropdown>
</div>
<div class="hidden flex-wrap items-center gap-2 md:flex">
<div class="hidden flex-wrap items-center gap-2 md:flex">
<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="$wire.dispatch('restartEvent')">
<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">
@@ -117,25 +381,23 @@
</svg>
Restart
</x-forms.button>
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
<x-slot:button-title>
<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">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
Stop
</x-slot:button-title>
</x-modal-confirmation>
<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">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
Stop
</x-forms.button>
@elseif (str($service->status)->contains('degraded'))
<x-forms.button title="Restart" @click="$wire.dispatch('restartEvent')">
<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">
@@ -145,25 +407,23 @@
</svg>
Restart
</x-forms.button>
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
<x-slot:button-title>
<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">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
Stop
</x-slot:button-title>
</x-modal-confirmation>
<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">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
Stop
</x-forms.button>
@elseif (str($service->status)->contains('exited'))
<button @click="$wire.dispatch('startEvent')" 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">
@@ -171,26 +431,24 @@
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</button>
</x-forms.button>
@else
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
<x-slot:button-title>
<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">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
Stop
</x-slot:button-title>
</x-modal-confirmation>
<button @click="$wire.dispatch('startEvent')" class="gap-2 button">
<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">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
Stop
</x-forms.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">
@@ -198,7 +456,7 @@
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</button>
</x-forms.button>
@endif
</div>
</div>
@@ -212,6 +470,51 @@
</div>
@endif
</div>
@if ($service->isDeployable)
<x-modal-confirmation title="Confirm Service Deployment?" buttonTitle="Deploy"
submitAction="startEvent" :dispatchAction="true" :actions="['This service will be deployed.']"
:confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Confirm">
<x-slot:trigger>
<button id="service-start-trigger" type="button" class="hidden">Deploy</button>
</x-slot:trigger>
</x-modal-confirmation>
<x-modal-confirmation title="Confirm Service Restart?" buttonTitle="Restart"
submitAction="restartEvent" :dispatchAction="true" :actions="['This service will be restarted.']"
:confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Confirm">
<x-slot:trigger>
<button id="service-restart-trigger" type="button" class="hidden">Restart</button>
</x-slot:trigger>
</x-modal-confirmation>
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop"
submitAction="stop" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
<x-slot:trigger>
<button id="service-stop-trigger" type="button" class="hidden">Stop</button>
</x-slot:trigger>
</x-modal-confirmation>
<x-modal-confirmation title="Confirm Service Force Deployment?" buttonTitle="Force Deploy"
submitAction="forceDeployEvent" :dispatchAction="true" :actions="['This service will be force deployed.']"
:confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Confirm">
<x-slot:trigger>
<button id="service-forceDeploy-trigger" type="button" class="hidden">Force Deploy</button>
</x-slot:trigger>
</x-modal-confirmation>
<x-modal-confirmation title="Confirm Pull Latest Images & Restart?" buttonTitle="Pull Latest Images & Restart"
submitAction="pullAndRestartEvent" :dispatchAction="true" :actions="['Latest images will be pulled and the service will be restarted.']"
:confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Confirm">
<x-slot:trigger>
<button id="service-pullAndRestart-trigger" type="button" class="hidden">Pull Latest Images & Restart</button>
</x-slot:trigger>
</x-modal-confirmation>
<x-modal-confirmation title="Confirm Force Cleanup Containers?" buttonTitle="Force Cleanup Containers"
submitAction="cleanupEvent" :dispatchAction="true" :actions="['Service containers will be force cleaned up.']"
:confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Confirm">
<x-slot:trigger>
<button id="service-cleanup-trigger" type="button" class="hidden">Force Cleanup Containers</button>
</x-slot:trigger>
</x-modal-confirmation>
@endif
@script
<script>
$wire.$on('stopEvent', () => {
@@ -227,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 () => {
@@ -244,14 +545,15 @@
}
$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', () => {
$wire.$call('stop', true);
});
$wire.on('imagePulled', () => {
window.dispatchEvent(new CustomEvent('startservice'));
$wire.$dispatch('info', 'Restarting service.');
@@ -1,10 +1,10 @@
<div>
<livewire:project.service.heading :service="$service" :parameters="$parameters" :query="$query" />
<div class="flex flex-col h-full gap-8 sm:flex-row">
<div class="flex flex-col h-full gap-4 md:gap-8 md:flex-row">
@if ($resourceType === 'database')
<x-service-database.sidebar :parameters="$parameters" :serviceDatabase="$serviceDatabase" :isImportSupported="$isImportSupported" />
@else
<div class="sub-menu-wrapper">
<div class="sub-menu-wrapper hidden md:flex">
<a class="sub-menu-item"
class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-active' : '' }}"
{{ wireNavigate() }}
@@ -43,7 +43,15 @@
@endcan
</span>
@endif
<div @class(['pt-2' => $isApplication, 'text-xs'])>{{ formatContainerStatus($resource->status) }}</div>
<div @class(['pt-2' => $isApplication])>
@if (str($resource->status)->contains('running'))
<x-status-badge status="{{ formatContainerStatus($resource->status) }}" type="success" />
@elseif (str($resource->status)->contains(['starting', 'restarting', 'degraded']))
<x-status-badge status="{{ formatContainerStatus($resource->status) }}" type="warning" />
@else
<x-status-badge status="{{ formatContainerStatus($resource->status) }}" type="error" />
@endif
</div>
</div>
<div class="flex items-center px-4">
@if ($isDatabase && ($resource->isBackupSolutionAvailable() || $resource->is_migrated))
@@ -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
@@ -12,65 +12,53 @@
<livewire:activity-monitor header="Logs" fullHeight />
</x-slot:content>
</x-slide-over>
<div class="flex items-center gap-2">
<h1>Server</h1>
@if ($server->proxySet())
<div class="flex">
<div class="flex items-center">
@if ($proxyStatus === 'running')
<x-status.running status="Proxy Running" noLoading />
@elseif ($proxyStatus === 'restarting')
<x-status.restarting status="Proxy Restarting" noLoading />
@elseif ($proxyStatus === 'stopping')
<x-status.restarting status="Proxy Stopping" noLoading />
@elseif ($proxyStatus === 'starting')
<x-status.restarting status="Proxy Starting" noLoading />
@elseif (data_get($server, 'proxy.force_stop'))
<div wire:loading.remove wire:target="checkProxy">
<x-status.stopped status="Proxy Stopped (Force Stop)" noLoading />
</div>
@elseif ($proxyStatus === 'exited')
<div wire:loading.remove wire:target="checkProxy">
<x-status.stopped status="Proxy Exited" noLoading />
<div class="flex flex-col gap-1.5">
<div class="flex flex-wrap items-center gap-2">
<h1>Server</h1>
@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">
@if ($proxyStatus === 'running')
<x-status-badge label="Proxy" status="Running" type="success" />
@elseif ($proxyStatus === 'restarting')
<x-status-badge label="Proxy" status="Restarting" type="warning" />
@elseif ($proxyStatus === 'stopping')
<x-status-badge label="Proxy" status="Stopping" type="warning" />
@elseif ($proxyStatus === 'starting')
<x-status-badge label="Proxy" status="Starting" type="warning" />
@elseif (data_get($server, 'proxy.force_stop'))
<x-status-badge wire:loading.remove wire:target="checkProxy" label="Proxy"
status="Force stopped" type="error" />
@elseif ($proxyStatus === 'exited')
<x-status-badge wire:loading.remove wire:target="checkProxy" label="Proxy" status="Exited"
type="error" />
@endif
<x-status-badge wire:loading wire:target="checkProxy" label="Proxy" status="Checking..."
type="warning" />
</div>
@endif
<div wire:loading wire:target="checkProxy" class="badge badge-warning"></div>
<div wire:loading wire:target="checkProxy"
class="pl-2 pr-1 text-xs font-bold tracking-wider dark:text-warning">
Checking Ports Availability...
</div>
@if ($proxyStatus !== 'exited')
<button wire:loading.remove title="Refresh Status" wire:click='checkProxyStatus'
class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
</svg>
</button>
<button wire:loading title="Refreshing Status" wire:click='checkProxyStatus'
class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
<svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
</svg>
</button>
@if ($showSentinelStatus)
@if ($server->isSentinelLive())
<x-status-badge label="Sentinel" status="In sync" type="success" />
@else
<x-status-badge label="Sentinel" status="Out of sync" type="error" />
@endif
@endif
@if ($server->proxySet())
<x-status-badge as="button" wire:target="checkProxyStatus" wire:loading.attr="disabled"
wire:click='checkProxyStatus' status="Refresh" type="neutral" title="Refresh Status"
aria-label="Refresh proxy status"
class="min-w-[4.5rem] justify-center cursor-pointer border-transparent hover:bg-neutral-200 disabled:cursor-wait disabled:opacity-70 dark:hover:bg-coolgray-300" />
@endif
</div>
</div>
@endif
@if ($server->isSentinelEnabled())
<div class="flex">
<div class="flex items-center">
@if ($server->isSentinelLive())
<x-status.running status="Sentinel In Sync" noLoading />
@else
<x-status.stopped status="Sentinel Out of Sync" noLoading />
@endif
</div>
</div>
@endif
@endif
</div>
<div class="subtitle">{{ data_get($server, 'name') }}</div>
</div>
<div class="subtitle">{{ data_get($server, 'name') }}</div>
<div class="navbar-main">
<nav
class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar min-h-10 whitespace-nowrap pt-2">
@@ -93,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() }}>
@@ -129,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">
@@ -193,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
@@ -189,6 +189,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>
@@ -373,7 +373,8 @@
baseUrl = devWebhook;
}
const webhookBaseUrl = `${baseUrl}/webhooks`;
const path = organization ? `organizations/${organization}/settings/apps/new` : 'settings/apps/new';
const organizationPath = organization ? encodeURIComponent(organization.replace(/^\/+|\/+$/g, '')) : '';
const path = organizationPath ? `organizations/${organizationPath}/settings/apps/new` : 'settings/apps/new';
const default_permissions = {
contents: 'read',
metadata: 'read',
@@ -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">
@@ -230,15 +230,37 @@
]" confirmationText="{{ currentTeam()->name }}"
confirmationLabel="Enter your team name to confirm"
shortConfirmationLabel="Team Name" step2ButtonText="Confirm Cancellation" />
<x-modal-confirmation title="Cancel Immediately?" buttonTitle="Cancel Immediately"
isErrorButton submitAction="cancelImmediately"
:actions="[
'Your subscription will be cancelled immediately.',
'All servers will be deactivated.',
'No refund will be issued for the remaining period.',
]" confirmationText="{{ currentTeam()->name }}"
confirmationLabel="Enter your team name to confirm"
shortConfirmationLabel="Team Name" step2ButtonText="Permanently Cancel" />
@if ($isRefundEligible)
<div wire:key="cancel-immediately-refundable">
<x-modal-confirmation title="Cancel Immediately?" buttonTitle="Cancel Immediately"
isErrorButton submitAction="cancelImmediately"
:checkboxes="[
[
'id' => 'refundLatestPayment',
'label' => 'Refund my latest payment (eligible for '.$refundDaysRemaining.' more days).',
'default_warning' => 'No refund will be issued for the remaining period.',
],
]"
:actions="[
'Your subscription will be cancelled immediately.',
'All servers will be deactivated.',
]" confirmationText="{{ currentTeam()->name }}"
confirmationLabel="Enter your team name to confirm"
shortConfirmationLabel="Team Name" step2ButtonText="Permanently Cancel" />
</div>
@else
<div wire:key="cancel-immediately-standard">
<x-modal-confirmation title="Cancel Immediately?" buttonTitle="Cancel Immediately"
isErrorButton submitAction="cancelImmediately"
:actions="[
'Your subscription will be cancelled immediately.',
'All servers will be deactivated.',
'No refund will be issued for the remaining period.',
]" confirmationText="{{ currentTeam()->name }}"
confirmationLabel="Enter your team name to confirm"
shortConfirmationLabel="Team Name" step2ButtonText="Permanently Cancel" />
</div>
@endif
@endif
</div>
@if (currentTeam()->subscription->stripe_cancel_at_period_end)
@@ -249,7 +271,7 @@
{{-- Refund --}}
<section>
<h3 class="pb-2">Refund</h3>
@if ($refundCheckLoading || ($isRefundEligible && !currentTeam()->subscription->stripe_cancel_at_period_end))
@if ($refundCheckLoading || $isRefundEligible)
<div class="flex flex-wrap items-center gap-2">
@if ($refundCheckLoading)
<x-forms.button disabled>Request Full Refund</x-forms.button>
@@ -269,7 +291,7 @@
<p class="mt-2 text-sm text-neutral-500">
@if ($refundCheckLoading)
Checking refund eligibility...
@elseif ($isRefundEligible && !currentTeam()->subscription->stripe_cancel_at_period_end)
@elseif ($isRefundEligible)
Eligible for a full refund &mdash; <strong class="dark:text-warning">{{ $refundDaysRemaining }}</strong> days remaining.
@elseif ($refundAlreadyUsed)
Refund already processed. Each team is eligible for one refund only.
@@ -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"