mirror of
https://github.com/tiennm99/coolify.git
synced 2026-04-17 17:21:04 +00:00
Merge branch 'next' into feat/shared-dev-view
This commit is contained in:
@@ -82,7 +82,7 @@
|
||||
*/
|
||||
html,
|
||||
body {
|
||||
@apply w-full min-h-full bg-neutral-50 dark:bg-base dark:text-neutral-400;
|
||||
@apply w-full min-h-full bg-gray-50 dark:bg-base dark:text-neutral-400;
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
@@ -32,7 +32,20 @@
|
||||
}
|
||||
|
||||
@utility input-sticky {
|
||||
@apply block py-1.5 w-full text-sm text-black rounded-sm border-0 ring-1 ring-inset dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300 focus-visible:outline-none focus-visible:border-l-4 focus-visible:border-l-coollabs dark:focus-visible:border-l-warning;
|
||||
@apply block py-1.5 w-full text-sm text-black rounded-sm border-0 dark:bg-coolgray-100 dark:text-white disabled:bg-neutral-200 disabled:text-neutral-500 dark:disabled:bg-coolgray-100/40 focus-visible:outline-none;
|
||||
box-shadow: inset 4px 0 0 transparent, inset 0 0 0 1px #e5e5e5;
|
||||
|
||||
&:where(.dark, .dark *) {
|
||||
box-shadow: inset 4px 0 0 transparent, inset 0 0 0 1px #242424;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: inset 4px 0 0 #6b16ed, inset 0 0 0 1px #e5e5e5;
|
||||
}
|
||||
|
||||
&:where(.dark, .dark *):focus-visible {
|
||||
box-shadow: inset 4px 0 0 #fcd452, inset 0 0 0 1px #242424;
|
||||
}
|
||||
}
|
||||
|
||||
@utility input-sticky-active {
|
||||
@@ -46,20 +59,49 @@
|
||||
|
||||
/* input, select before */
|
||||
@utility input-select {
|
||||
@apply block py-1.5 w-full text-sm text-black rounded-sm border-0 ring-2 ring-inset dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300 disabled:bg-neutral-200 disabled:text-neutral-500 dark:disabled:bg-coolgray-100/40 dark:disabled:ring-transparent;
|
||||
@apply block py-1.5 w-full text-sm text-black rounded-sm border-0 dark:bg-coolgray-100 dark:text-white disabled:bg-neutral-200 disabled:text-neutral-500 dark:disabled:bg-coolgray-100/40;
|
||||
box-shadow: inset 4px 0 0 transparent, inset 0 0 0 2px #e5e5e5;
|
||||
|
||||
&:where(.dark, .dark *) {
|
||||
box-shadow: inset 4px 0 0 transparent, inset 0 0 0 2px #242424;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:where(.dark, .dark *):disabled {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Readonly */
|
||||
@utility input {
|
||||
@apply dark:read-only:text-neutral-500 dark:read-only:ring-0 dark:read-only:bg-coolgray-100/40 placeholder:text-neutral-300 dark:placeholder:text-neutral-700 read-only:text-neutral-500 read-only:bg-neutral-200;
|
||||
@apply dark:read-only:text-neutral-500 dark:read-only:bg-coolgray-100/40 placeholder:text-neutral-300 dark:placeholder:text-neutral-700 read-only:text-neutral-500 read-only:bg-neutral-200;
|
||||
@apply input-select;
|
||||
@apply focus-visible:outline-none focus-visible:border-l-4 focus-visible:border-l-coollabs dark:focus-visible:border-l-warning;
|
||||
@apply focus-visible:outline-none;
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: inset 4px 0 0 #6b16ed, inset 0 0 0 2px #e5e5e5;
|
||||
}
|
||||
|
||||
&:where(.dark, .dark *):focus-visible {
|
||||
box-shadow: inset 4px 0 0 #fcd452, inset 0 0 0 2px #242424;
|
||||
}
|
||||
|
||||
&:read-only {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:where(.dark, .dark *):read-only {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
@utility select {
|
||||
@apply w-full;
|
||||
@apply input-select;
|
||||
@apply focus-visible:outline-none focus-visible:border-l-4 focus-visible:border-l-coollabs dark:focus-visible:border-l-warning;
|
||||
@apply focus-visible:outline-none;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='%23000000'%3e%3cpath stroke-linecap='round' stroke-linejoin='round' d='M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9'/%3e%3c/svg%3e");
|
||||
background-position: right 0.5rem center;
|
||||
background-repeat: no-repeat;
|
||||
@@ -69,6 +111,14 @@
|
||||
&:where(.dark, .dark *) {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='%23ffffff'%3e%3cpath stroke-linecap='round' stroke-linejoin='round' d='M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: inset 4px 0 0 #6b16ed, inset 0 0 0 2px #e5e5e5;
|
||||
}
|
||||
|
||||
&:where(.dark, .dark *):focus-visible {
|
||||
box-shadow: inset 4px 0 0 #fcd452, inset 0 0 0 2px #242424;
|
||||
}
|
||||
}
|
||||
|
||||
@utility button {
|
||||
@@ -152,7 +202,7 @@
|
||||
}
|
||||
|
||||
@utility navbar-main {
|
||||
@apply flex flex-col gap-4 justify-items-start pb-2 border-b-2 border-solid h-fit md:flex-row sm:justify-between dark:border-coolgray-200 border-neutral-200 md:items-center;
|
||||
@apply flex flex-col gap-4 justify-items-start pb-2 border-b-2 border-solid h-fit md:flex-row sm:justify-between dark:border-coolgray-200 border-neutral-200 md:items-center text-neutral-700 dark:text-neutral-400;
|
||||
}
|
||||
|
||||
@utility loading {
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<div class="w-full border-t border-neutral-300 dark:border-coolgray-400"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-2 dark:bg-base text-neutral-500 dark:text-neutral-400">
|
||||
<span class="px-2 bg-gray-50 dark:bg-base text-neutral-500 dark:text-neutral-400 ">
|
||||
Don't have an account?
|
||||
</span>
|
||||
</div>
|
||||
@@ -82,7 +82,7 @@
|
||||
<div class="w-full border-t border-neutral-300 dark:border-coolgray-400"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-2 dark:bg-base text-neutral-500 dark:text-neutral-400">or
|
||||
<span class="px-2 bg-gray-50 dark:bg-base text-neutral-500 dark:text-neutral-400">or
|
||||
continue with</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,8 @@ $email = getOldOrLocal('email', 'test3@example.com');
|
||||
</svg>
|
||||
<div>
|
||||
<p class="font-bold text-warning">Root User Setup</p>
|
||||
<p class="text-sm dark:text-white text-black">This user will be the root user with full admin access.</p>
|
||||
<p class="text-sm dark:text-white text-black">This user will be the root user with full
|
||||
admin access.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,13 +59,16 @@ $email = getOldOrLocal('email', 'test3@example.com');
|
||||
<x-forms.input id="password_confirmation" required type="password" name="password_confirmation"
|
||||
label="{{ __('input.password.again') }}" />
|
||||
|
||||
<div class="p-4 bg-neutral-50 dark:bg-coolgray-200 rounded-lg border border-neutral-200 dark:border-coolgray-400">
|
||||
<div
|
||||
class="p-4 bg-neutral-50 dark:bg-coolgray-200 rounded-lg border border-neutral-200 dark:border-coolgray-400">
|
||||
<p class="text-xs dark:text-neutral-400">
|
||||
Your password should be min 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one symbol.
|
||||
Your password should be min 8 characters long and contain at least one uppercase letter,
|
||||
one lowercase letter, one number, and one symbol.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<x-forms.button class="w-full justify-center py-3 box-boarding mt-2" type="submit" isHighlighted>
|
||||
<x-forms.button class="w-full justify-center py-3 box-boarding mt-2" type="submit"
|
||||
isHighlighted>
|
||||
Create Account
|
||||
</x-forms.button>
|
||||
</form>
|
||||
@@ -74,17 +78,18 @@ $email = getOldOrLocal('email', 'test3@example.com');
|
||||
<div class="w-full border-t border-neutral-300 dark:border-coolgray-400"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-2 dark:bg-base text-neutral-500 dark:text-neutral-400">
|
||||
<span class="px-2 bg-gray-50 dark:bg-base text-neutral-500 dark:text-neutral-400">
|
||||
Already have an account?
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="/login" class="block w-full text-center py-3 px-4 rounded-lg border border-neutral-300 dark:border-coolgray-400 font-medium hover:border-coollabs dark:hover:border-warning transition-colors">
|
||||
<a href="/login"
|
||||
class="block w-full text-center py-3 px-4 rounded-lg border border-neutral-300 dark:border-coolgray-400 font-medium hover:border-coollabs dark:hover:border-warning transition-colors">
|
||||
{{ __('auth.already_registered') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</x-layout-simple>
|
||||
</x-layout-simple>
|
||||
@@ -47,16 +47,19 @@
|
||||
label="{{ __('input.email') }}" />
|
||||
<x-forms.input required type="password" id="password" name="password"
|
||||
label="{{ __('input.password') }}" />
|
||||
<x-forms.input required type="password" id="password_confirmation"
|
||||
name="password_confirmation" label="{{ __('input.password.again') }}" />
|
||||
<x-forms.input required type="password" id="password_confirmation" name="password_confirmation"
|
||||
label="{{ __('input.password.again') }}" />
|
||||
|
||||
<div class="p-4 bg-neutral-50 dark:bg-coolgray-200 rounded-lg border border-neutral-200 dark:border-coolgray-400">
|
||||
<div
|
||||
class="p-4 bg-neutral-50 dark:bg-coolgray-200 rounded-lg border border-neutral-200 dark:border-coolgray-400">
|
||||
<p class="text-xs dark:text-neutral-400">
|
||||
Your password should be min 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one symbol.
|
||||
Your password should be min 8 characters long and contain at least one uppercase letter,
|
||||
one lowercase letter, one number, and one symbol.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<x-forms.button class="w-full justify-center py-3 box-boarding mt-2" type="submit" isHighlighted>
|
||||
<x-forms.button class="w-full justify-center py-3 box-boarding mt-2" type="submit"
|
||||
isHighlighted>
|
||||
{{ __('auth.reset_password') }}
|
||||
</x-forms.button>
|
||||
</form>
|
||||
@@ -66,17 +69,18 @@
|
||||
<div class="w-full border-t border-neutral-300 dark:border-coolgray-400"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-2 dark:bg-base text-neutral-500 dark:text-neutral-400">
|
||||
<span class="px-2 bg-gray-50 dark:bg-base text-neutral-500 dark:text-neutral-400">
|
||||
Remember your password?
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="/login" class="block w-full text-center py-3 px-4 rounded-lg border border-neutral-300 dark:border-coolgray-400 font-medium hover:border-coollabs dark:hover:border-warning transition-colors">
|
||||
<a href="/login"
|
||||
class="block w-full text-center py-3 px-4 rounded-lg border border-neutral-300 dark:border-coolgray-400 font-medium hover:border-coollabs dark:hover:border-warning transition-colors">
|
||||
Back to Login
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</x-layout-simple>
|
||||
</x-layout-simple>
|
||||
@@ -120,7 +120,7 @@
|
||||
<div class="w-full border-t border-neutral-300 dark:border-coolgray-400"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-2 dark:bg-base text-neutral-500 dark:text-neutral-400">
|
||||
<span class="px-2 bg-gray-50 dark:bg-base text-neutral-500 dark:text-neutral-400">
|
||||
Need help?
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
@props(['type' => 'warning', 'title' => 'Warning', 'class' => ''])
|
||||
@props(['type' => 'warning', 'title' => 'Warning', 'class' => '', 'dismissible' => false, 'onDismiss' => null])
|
||||
|
||||
@php
|
||||
$icons = [
|
||||
'warning' => '<svg class="w-5 h-5 text-yellow-600 dark:text-yellow-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>',
|
||||
|
||||
|
||||
'danger' => '<svg class="w-5 h-5 text-red-600 dark:text-red-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path></svg>',
|
||||
|
||||
|
||||
'info' => '<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>',
|
||||
|
||||
|
||||
'success' => '<svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path></svg>'
|
||||
];
|
||||
|
||||
@@ -42,12 +42,12 @@
|
||||
$icon = $icons[$type] ?? $icons['warning'];
|
||||
@endphp
|
||||
|
||||
<div {{ $attributes->merge(['class' => 'p-4 border rounded-lg ' . $colorScheme['bg'] . ' ' . $colorScheme['border'] . ' ' . $class]) }}>
|
||||
<div {{ $attributes->merge(['class' => 'relative p-4 border rounded-lg ' . $colorScheme['bg'] . ' ' . $colorScheme['border'] . ' ' . $class]) }}>
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
{!! $icon !!}
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<div class="ml-3 {{ $dismissible ? 'pr-8' : '' }}">
|
||||
<div class="text-base font-bold {{ $colorScheme['title'] }}">
|
||||
{{ $title }}
|
||||
</div>
|
||||
@@ -55,5 +55,15 @@
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
@if($dismissible && $onDismiss)
|
||||
<button type="button" @click.stop="{{ $onDismiss }}"
|
||||
class="absolute top-2 right-2 p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 transition-colors"
|
||||
aria-label="Dismiss">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
|
||||
stroke="currentColor" class="w-4 h-4 {{ $colorScheme['text'] }}">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,12 +97,14 @@
|
||||
}" @click.outside="open = false" class="relative">
|
||||
|
||||
{{-- Unified Input Container with Tags Inside --}}
|
||||
<div @click="$refs.searchInput.focus()"
|
||||
class="flex flex-wrap gap-1.5 max-h-40 overflow-y-auto scrollbar py-1.5 px-2 w-full text-sm rounded-sm border-0 ring-2 ring-inset ring-neutral-200 dark:ring-coolgray-300 bg-white dark:bg-coolgray-100 cursor-text px-1 focus-within:border-l-4 focus-within:border-l-coollabs dark:focus-within:border-l-warning text-black dark:text-white"
|
||||
<div @click="$refs.searchInput.focus()" x-data="{ focused: false }" @focusin="focused = true" @focusout="focused = false"
|
||||
class="flex flex-wrap gap-1.5 max-h-40 overflow-y-auto scrollbar py-1.5 px-2 w-full text-sm rounded-sm border-0 bg-white dark:bg-coolgray-100 cursor-text px-1 text-black dark:text-white"
|
||||
:style="focused ? 'box-shadow: inset 4px 0 0 #6b16ed, inset 0 0 0 2px #e5e5e5;' : 'box-shadow: inset 4px 0 0 transparent, inset 0 0 0 2px #e5e5e5;'"
|
||||
x-init="$watch('focused', () => { if ($root.classList.contains('dark') || document.documentElement.classList.contains('dark')) { $el.style.boxShadow = focused ? 'inset 4px 0 0 #fcd452, inset 0 0 0 2px #242424' : 'inset 4px 0 0 transparent, inset 0 0 0 2px #242424'; } })"
|
||||
:class="{
|
||||
'opacity-50': {{ $disabled ? 'true' : 'false' }}
|
||||
}" wire:loading.class="opacity-50"
|
||||
wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4">
|
||||
wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]">
|
||||
|
||||
{{-- Selected Tags Inside Input --}}
|
||||
<template x-for="value in selected" :key="value">
|
||||
@@ -221,11 +223,13 @@
|
||||
<input type="hidden" :value="selected" @required($required) />
|
||||
|
||||
{{-- Input Container --}}
|
||||
<div @click="openDropdown()"
|
||||
class="flex items-center gap-2 py-1.5 w-full text-sm rounded-sm border-0 ring-2 ring-inset ring-neutral-200 dark:ring-coolgray-300 bg-white dark:bg-coolgray-100 cursor-text focus-within:border-l-4 focus-within:border-l-coollabs dark:focus-within:border-l-warning text-black dark:text-white"
|
||||
<div @click="openDropdown()" x-data="{ focused: false }" @focusin="focused = true" @focusout="focused = false"
|
||||
class="flex items-center gap-2 py-1.5 w-full text-sm rounded-sm border-0 bg-white dark:bg-coolgray-100 cursor-text text-black dark:text-white"
|
||||
:style="focused ? 'box-shadow: inset 4px 0 0 #6b16ed, inset 0 0 0 2px #e5e5e5;' : 'box-shadow: inset 4px 0 0 transparent, inset 0 0 0 2px #e5e5e5;'"
|
||||
x-init="$watch('focused', () => { if ($root.classList.contains('dark') || document.documentElement.classList.contains('dark')) { $el.style.boxShadow = focused ? 'inset 4px 0 0 #fcd452, inset 0 0 0 2px #242424' : 'inset 4px 0 0 transparent, inset 0 0 0 2px #242424'; } })"
|
||||
:class="{
|
||||
'opacity-50': {{ $disabled ? 'true' : 'false' }}
|
||||
}" wire:loading.class="opacity-50" wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4">
|
||||
}" wire:loading.class="opacity-50" wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]">
|
||||
|
||||
{{-- Display Selected Value or Search Input --}}
|
||||
<div class="flex-1 flex items-center min-w-0 px-1">
|
||||
|
||||
268
resources/views/components/forms/env-var-input.blade.php
Normal file
268
resources/views/components/forms/env-var-input.blade.php
Normal file
@@ -0,0 +1,268 @@
|
||||
<div class="w-full">
|
||||
@if ($label)
|
||||
<label class="flex gap-1 items-center mb-1 text-sm font-medium">{{ $label }}
|
||||
@if ($required)
|
||||
<x-highlighted text="*" />
|
||||
@endif
|
||||
@if ($helper)
|
||||
<x-helper :helper="$helper" />
|
||||
@endif
|
||||
</label>
|
||||
@endif
|
||||
|
||||
<div x-data="{
|
||||
showDropdown: false,
|
||||
suggestions: [],
|
||||
selectedIndex: 0,
|
||||
cursorPosition: 0,
|
||||
currentScope: null,
|
||||
availableScopes: ['team', 'project', 'environment'],
|
||||
availableVars: @js($availableVars),
|
||||
scopeUrls: @js($scopeUrls),
|
||||
|
||||
isAutocompleteDisabled() {
|
||||
const hasAnyVars = Object.values(this.availableVars).some(vars => vars.length > 0);
|
||||
return !hasAnyVars;
|
||||
},
|
||||
|
||||
handleInput() {
|
||||
const input = this.$refs.input;
|
||||
if (!input) return;
|
||||
|
||||
const value = input.value || '';
|
||||
|
||||
if (this.isAutocompleteDisabled()) {
|
||||
this.showDropdown = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.cursorPosition = input.selectionStart || 0;
|
||||
const textBeforeCursor = value.substring(0, this.cursorPosition);
|
||||
|
||||
const openBraces = '{' + '{';
|
||||
const lastBraceIndex = textBeforeCursor.lastIndexOf(openBraces);
|
||||
|
||||
if (lastBraceIndex === -1) {
|
||||
this.showDropdown = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastBraceIndex > 0 && textBeforeCursor[lastBraceIndex - 1] === '{') {
|
||||
this.showDropdown = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const textAfterBrace = textBeforeCursor.substring(lastBraceIndex);
|
||||
const closeBraces = '}' + '}';
|
||||
if (textAfterBrace.includes(closeBraces)) {
|
||||
this.showDropdown = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const content = textAfterBrace.substring(2).trim();
|
||||
|
||||
if (content === '') {
|
||||
this.currentScope = null;
|
||||
this.suggestions = this.availableScopes.map(scope => ({
|
||||
type: 'scope',
|
||||
value: scope,
|
||||
display: scope
|
||||
}));
|
||||
this.selectedIndex = 0;
|
||||
this.showDropdown = true;
|
||||
} else if (content.includes('.')) {
|
||||
const [scope, partial] = content.split('.');
|
||||
|
||||
if (!this.availableScopes.includes(scope)) {
|
||||
this.showDropdown = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentScope = scope;
|
||||
const scopeVars = this.availableVars[scope] || [];
|
||||
const filtered = scopeVars.filter(v =>
|
||||
v.toLowerCase().includes((partial || '').toLowerCase())
|
||||
);
|
||||
|
||||
if (filtered.length === 0 && scopeVars.length === 0) {
|
||||
this.suggestions = [];
|
||||
this.showDropdown = true;
|
||||
} else {
|
||||
this.suggestions = filtered.map(varName => ({
|
||||
type: 'variable',
|
||||
value: varName,
|
||||
display: `${scope}.${varName}`,
|
||||
scope: scope
|
||||
}));
|
||||
this.selectedIndex = 0;
|
||||
this.showDropdown = filtered.length > 0;
|
||||
}
|
||||
} else {
|
||||
this.currentScope = null;
|
||||
const filtered = this.availableScopes.filter(scope =>
|
||||
scope.toLowerCase().includes(content.toLowerCase())
|
||||
);
|
||||
|
||||
this.suggestions = filtered.map(scope => ({
|
||||
type: 'scope',
|
||||
value: scope,
|
||||
display: scope
|
||||
}));
|
||||
this.selectedIndex = 0;
|
||||
this.showDropdown = filtered.length > 0;
|
||||
}
|
||||
},
|
||||
|
||||
getScopeUrl(scope) {
|
||||
return this.scopeUrls[scope] || this.scopeUrls['default'];
|
||||
},
|
||||
|
||||
selectSuggestion(suggestion) {
|
||||
const input = this.$refs.input;
|
||||
if (!input) return;
|
||||
|
||||
const value = input.value || '';
|
||||
const textBeforeCursor = value.substring(0, this.cursorPosition);
|
||||
const textAfterCursor = value.substring(this.cursorPosition);
|
||||
const openBraces = '{' + '{';
|
||||
const lastBraceIndex = textBeforeCursor.lastIndexOf(openBraces);
|
||||
|
||||
if (suggestion.type === 'scope') {
|
||||
const newText = value.substring(0, lastBraceIndex) +
|
||||
openBraces + ' ' + suggestion.value + '.' +
|
||||
textAfterCursor;
|
||||
input.value = newText;
|
||||
this.cursorPosition = lastBraceIndex + 3 + suggestion.value.length + 1;
|
||||
|
||||
this.$nextTick(() => {
|
||||
input.setSelectionRange(this.cursorPosition, this.cursorPosition);
|
||||
input.focus();
|
||||
this.handleInput();
|
||||
});
|
||||
} else {
|
||||
const closeBraces = '}' + '}';
|
||||
const newText = value.substring(0, lastBraceIndex) +
|
||||
openBraces + ' ' + suggestion.display + ' ' + closeBraces +
|
||||
textAfterCursor;
|
||||
input.value = newText;
|
||||
this.cursorPosition = lastBraceIndex + 3 + suggestion.display.length + 3;
|
||||
|
||||
input.dispatchEvent(new Event('input'));
|
||||
|
||||
this.showDropdown = false;
|
||||
this.selectedIndex = 0;
|
||||
|
||||
this.$nextTick(() => {
|
||||
input.setSelectionRange(this.cursorPosition, this.cursorPosition);
|
||||
input.focus();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleKeydown(event) {
|
||||
if (!this.showDropdown) return;
|
||||
if (!this.suggestions || this.suggestions.length === 0) return;
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
this.selectedIndex = (this.selectedIndex + 1) % this.suggestions.length;
|
||||
this.$nextTick(() => {
|
||||
const el = document.getElementById('suggestion-' + this.selectedIndex);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
});
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
this.selectedIndex = this.selectedIndex <= 0 ? this.suggestions.length - 1 : this.selectedIndex - 1;
|
||||
this.$nextTick(() => {
|
||||
const el = document.getElementById('suggestion-' + this.selectedIndex);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
});
|
||||
} else if (event.key === 'Enter' && this.showDropdown) {
|
||||
event.preventDefault();
|
||||
if (this.suggestions[this.selectedIndex]) {
|
||||
this.selectSuggestion(this.suggestions[this.selectedIndex]);
|
||||
}
|
||||
} else if (event.key === 'Escape') {
|
||||
this.showDropdown = false;
|
||||
}
|
||||
}
|
||||
}"
|
||||
@click.outside="showDropdown = false"
|
||||
class="relative">
|
||||
|
||||
<input
|
||||
x-ref="input"
|
||||
@input="handleInput()"
|
||||
@keydown="handleKeydown($event)"
|
||||
@click="handleInput()"
|
||||
autocomplete="{{ $autocomplete }}"
|
||||
{{ $attributes->merge(['class' => $defaultClass]) }}
|
||||
@required($required)
|
||||
@readonly($readonly)
|
||||
@if ($modelBinding !== 'null')
|
||||
wire:model="{{ $modelBinding }}"
|
||||
wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4"
|
||||
@endif
|
||||
wire:loading.attr="disabled"
|
||||
type="{{ $type }}"
|
||||
@disabled($disabled)
|
||||
@if ($htmlId !== 'null') id="{{ $htmlId }}" @endif
|
||||
name="{{ $name }}"
|
||||
placeholder="{{ $attributes->get('placeholder') }}"
|
||||
@if ($autofocus) autofocus @endif>
|
||||
|
||||
{{-- Dropdown for suggestions --}}
|
||||
<div x-show="showDropdown"
|
||||
x-transition
|
||||
class="absolute z-[60] w-full mt-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-400 rounded shadow-lg">
|
||||
|
||||
<template x-if="suggestions.length === 0 && currentScope">
|
||||
<div class="px-3 py-2 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
<div>No shared variables found in <span class="font-semibold" x-text="currentScope"></span> scope.</div>
|
||||
<a :href="getScopeUrl(currentScope)"
|
||||
class="text-coollabs dark:text-warning hover:underline text-xs mt-1 inline-block"
|
||||
target="_blank">
|
||||
Add <span x-text="currentScope"></span> variables →
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div x-show="suggestions.length > 0"
|
||||
x-ref="dropdownList"
|
||||
class="max-h-48 overflow-y-scroll"
|
||||
style="scrollbar-width: thin;">
|
||||
<template x-for="(suggestion, index) in suggestions" :key="index">
|
||||
<div :id="'suggestion-' + index"
|
||||
@click="selectSuggestion(suggestion)"
|
||||
class="px-3 py-2 cursor-pointer hover:bg-neutral-100 dark:hover:bg-coolgray-200 flex items-center gap-2"
|
||||
:class="{ 'bg-neutral-50 dark:bg-coolgray-300': index === selectedIndex }">
|
||||
<template x-if="suggestion.type === 'scope'">
|
||||
<span class="text-xs px-2 py-0.5 bg-coollabs/10 dark:bg-warning/10 text-coollabs dark:text-warning rounded">
|
||||
SCOPE
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="suggestion.type === 'variable'">
|
||||
<span class="text-xs px-2 py-0.5 bg-green-500/10 text-green-600 dark:text-green-400 rounded">
|
||||
VAR
|
||||
</span>
|
||||
</template>
|
||||
<span class="text-sm font-mono" x-text="suggestion.display"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!$label && $helper)
|
||||
<x-helper :helper="$helper" />
|
||||
@endif
|
||||
@error($modelBinding)
|
||||
<label class="label">
|
||||
<span class="text-red-500 label-text-alt">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
@@ -27,7 +27,7 @@
|
||||
@endif
|
||||
<input autocomplete="{{ $autocomplete }}" value="{{ $value }}"
|
||||
{{ $attributes->merge(['class' => $defaultClass]) }} @required($required)
|
||||
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @endif
|
||||
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif
|
||||
wire:loading.attr="disabled"
|
||||
type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $htmlId }}"
|
||||
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
|
||||
@@ -38,7 +38,7 @@
|
||||
@else
|
||||
<input autocomplete="{{ $autocomplete }}" @if ($value) value="{{ $value }}" @endif
|
||||
{{ $attributes->merge(['class' => $defaultClass]) }} @required($required) @readonly($readonly)
|
||||
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @endif
|
||||
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif
|
||||
wire:loading.attr="disabled"
|
||||
type="{{ $type }}" @disabled($disabled) min="{{ $attributes->get('min') }}"
|
||||
max="{{ $attributes->get('max') }}" minlength="{{ $attributes->get('minlength') }}"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
@endif
|
||||
<select {{ $attributes->merge(['class' => $defaultClass]) }} @disabled($disabled) @required($required)
|
||||
wire:loading.attr="disabled" name={{ $modelBinding }} id="{{ $htmlId }}"
|
||||
@if ($attributes->whereStartsWith('wire:model')->first()) {{ $attributes->whereStartsWith('wire:model')->first() }} wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @else wire:model={{ $modelBinding }} wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @endif>
|
||||
@if ($attributes->whereStartsWith('wire:model')->first()) {{ $attributes->whereStartsWith('wire:model')->first() }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @else wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif>
|
||||
{{ $slot }}
|
||||
</select>
|
||||
@error($modelBinding)
|
||||
|
||||
@@ -45,16 +45,16 @@
|
||||
@endif
|
||||
<input x-cloak x-show="type === 'password'" value="{{ $value }}"
|
||||
{{ $attributes->merge(['class' => $defaultClassInput]) }} @required($required)
|
||||
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @endif
|
||||
@if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif
|
||||
wire:loading.attr="disabled"
|
||||
type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $htmlId }}"
|
||||
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
|
||||
aria-placeholder="{{ $attributes->get('placeholder') }}">
|
||||
<textarea minlength="{{ $minlength }}" maxlength="{{ $maxlength }}" x-cloak x-show="type !== 'password'"
|
||||
placeholder="{{ $placeholder }}" {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $modelBinding }}" wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4"
|
||||
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $modelBinding }}" wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]"
|
||||
@else
|
||||
wire:model={{ $value ?? $modelBinding }} wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @endif
|
||||
wire:model={{ $value ?? $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif
|
||||
@disabled($disabled) @readonly($readonly) @required($required) id="{{ $htmlId }}"
|
||||
name="{{ $name }}" name={{ $modelBinding }}
|
||||
@if ($autofocus) x-ref="autofocusInput" @endif></textarea>
|
||||
@@ -64,9 +64,9 @@
|
||||
<textarea minlength="{{ $minlength }}" maxlength="{{ $maxlength }}"
|
||||
{{ $allowTab ? '@keydown.tab=handleKeydown' : '' }} placeholder="{{ $placeholder }}"
|
||||
{{ !$spellcheck ? 'spellcheck=false' : '' }} {{ $attributes->merge(['class' => $defaultClass]) }}
|
||||
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $modelBinding }}" wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4"
|
||||
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $modelBinding }}" wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]"
|
||||
@else
|
||||
wire:model={{ $value ?? $modelBinding }} wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @endif
|
||||
wire:model={{ $value ?? $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif
|
||||
@disabled($disabled) @readonly($readonly) @required($required) id="{{ $htmlId }}"
|
||||
name="{{ $name }}" name={{ $modelBinding }}
|
||||
@if ($autofocus) x-ref="autofocusInput" @endif></textarea>
|
||||
|
||||
@@ -8,8 +8,15 @@
|
||||
'content' => null,
|
||||
'closeOutside' => true,
|
||||
'minWidth' => '36rem',
|
||||
'maxWidth' => '48rem',
|
||||
'isFullWidth' => false,
|
||||
])
|
||||
|
||||
@php
|
||||
$modalId = 'modal-' . uniqid();
|
||||
@endphp
|
||||
|
||||
|
||||
<div x-data="{ modalOpen: false }"
|
||||
x-init="$watch('modalOpen', value => { if (!value) { $wire.dispatch('modalClosed') } })"
|
||||
:class="{ 'z-40': modalOpen }" @keydown.window.escape="modalOpen=false"
|
||||
@@ -38,14 +45,14 @@
|
||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
||||
@if ($closeOutside) @click="modalOpen=false" @endif
|
||||
class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
|
||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen"
|
||||
<div id="{{ $modalId }}" x-show="modalOpen" x-trap.inert.noscroll="modalOpen"
|
||||
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 border rounded-sm drop-shadow-sm min-w-full lg:min-w-[{{ $minWidth }}] max-w-fit max-h-[calc(100vh-2rem)] bg-white border-neutral-200 dark:bg-base dark:border-coolgray-300 flex flex-col">
|
||||
class="relative w-full min-w-full lg:min-w-[{{ $minWidth }}] max-w-[{{ $maxWidth }}] max-h-[calc(100vh-2rem)] border rounded-sm drop-shadow-sm bg-white border-neutral-200 dark:bg-base dark:border-coolgray-300 flex flex-col">
|
||||
<div class="flex items-center justify-between py-6 px-6 shrink-0">
|
||||
<h3 class="text-2xl font-bold">{{ $title }}</h3>
|
||||
<button @click="modalOpen=false"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@if ($server->proxySet())
|
||||
<div class="flex flex-col items-start gap-2 min-w-fit">
|
||||
<a class="{{ request()->routeIs('server.proxy') ? 'menu-item menu-item-active' : 'menu-item' }}"
|
||||
href="{{ route('server.proxy', $parameters) }}">
|
||||
<button>Configuration</button>
|
||||
</a>
|
||||
<div class="flex flex-col items-start gap-2 min-w-fit">
|
||||
<a class="{{ request()->routeIs('server.proxy') ? 'menu-item menu-item-active' : 'menu-item' }}"
|
||||
href="{{ route('server.proxy', $parameters) }}">
|
||||
<button>Configuration</button>
|
||||
</a>
|
||||
@if ($server->proxySet())
|
||||
<a class="{{ request()->routeIs('server.proxy.dynamic-confs') ? 'menu-item menu-item-active' : 'menu-item' }}"
|
||||
href="{{ route('server.proxy.dynamic-confs', $parameters) }}">
|
||||
<button>Dynamic Configurations</button>
|
||||
@@ -12,5 +12,5 @@
|
||||
href="{{ route('server.proxy.logs', $parameters) }}">
|
||||
<button>Logs</button>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,33 @@
|
||||
@props([
|
||||
'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
|
||||
$parts = explode(':', $status);
|
||||
$displayStatus = str($parts[0])->headline();
|
||||
$healthStatus = $parts[1] ?? null;
|
||||
} else {
|
||||
// Simple status without health
|
||||
$displayStatus = str($status)->headline();
|
||||
$healthStatus = null;
|
||||
}
|
||||
@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">
|
||||
{{ str($status)->before(':')->headline() }}
|
||||
</div>
|
||||
@if (!str($status)->startsWith('Proxy') && !str($status)->contains('('))
|
||||
<div class="text-xs dark:text-warning">({{ str($status)->after(':') }})</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>
|
||||
|
||||
@@ -5,13 +5,20 @@
|
||||
])
|
||||
@if (str($resource->status)->startsWith('running'))
|
||||
<x-status.running :status="$resource->status" :title="$title" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
@elseif(str($resource->status)->startsWith('restarting') ||
|
||||
str($resource->status)->startsWith('starting') ||
|
||||
str($resource->status)->startsWith('degraded'))
|
||||
@elseif(str($resource->status)->startsWith('degraded'))
|
||||
<x-status.degraded :status="$resource->status" :title="$title" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
@elseif(str($resource->status)->startsWith('restarting') || str($resource->status)->startsWith('starting'))
|
||||
<x-status.restarting :status="$resource->status" :title="$title" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
@else
|
||||
<x-status.stopped :status="$resource->status" />
|
||||
@endif
|
||||
@if (isset($resource->restart_count) && $resource->restart_count > 0 && !str($resource->status)->startsWith('exited'))
|
||||
<div class="flex items-center pl-2">
|
||||
<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>
|
||||
@endif
|
||||
@if (!str($resource->status)->contains('exited') && $showRefreshButton)
|
||||
<button wire:loading.remove.delay.shortest wire:target="manualCheckStatus" title="Refresh Status" wire:click='manualCheckStatus'
|
||||
class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
|
||||
|
||||
@@ -4,6 +4,26 @@
|
||||
'lastDeploymentLink' => null,
|
||||
'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
|
||||
$parts = explode(':', $status);
|
||||
$displayStatus = str($parts[0])->headline();
|
||||
$healthStatus = $parts[1] ?? null;
|
||||
} else {
|
||||
// Simple status without health
|
||||
$displayStatus = str($status)->headline();
|
||||
$healthStatus = null;
|
||||
}
|
||||
@endphp
|
||||
<div class="flex items-center">
|
||||
@if (!$noLoading)
|
||||
<x-loading wire:loading.delay.longer />
|
||||
@@ -13,14 +33,14 @@
|
||||
<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">
|
||||
{{ str($status)->before(':')->headline() }}
|
||||
{{ $displayStatus }}
|
||||
</a>
|
||||
@else
|
||||
{{ str($status)->before(':')->headline() }}
|
||||
{{ $displayStatus }}
|
||||
@endif
|
||||
</div>
|
||||
@if (!str($status)->startsWith('Proxy') && !str($status)->contains('('))
|
||||
<div class="text-xs dark:text-warning">({{ str($status)->after(':') }})</div>
|
||||
@if ($healthStatus && !str($displayStatus)->contains('('))
|
||||
<div class="text-xs dark:text-warning">({{ $healthStatus }})</div>
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,26 @@
|
||||
'lastDeploymentLink' => null,
|
||||
'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
|
||||
$parts = explode(':', $status);
|
||||
$displayStatus = str($parts[0])->headline();
|
||||
$healthStatus = $parts[1] ?? null;
|
||||
} else {
|
||||
// Simple status without health
|
||||
$displayStatus = str($status)->headline();
|
||||
$healthStatus = null;
|
||||
}
|
||||
@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>
|
||||
@@ -12,21 +32,27 @@
|
||||
@if ($title) title="{{ $title }}" @endif>
|
||||
@if ($lastDeploymentLink)
|
||||
<a href="{{ $lastDeploymentLink }}" target="_blank" class="underline cursor-pointer">
|
||||
{{ str($status)->before(':')->headline() }}
|
||||
{{ $displayStatus }}
|
||||
</a>
|
||||
@else
|
||||
{{ str($status)->before(':')->headline() }}
|
||||
{{ $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('(') &&
|
||||
str($status)->contains('unhealthy');
|
||||
(str($status)->contains('unhealthy') || str($healthStatus)->contains('unhealthy'));
|
||||
@endphp
|
||||
@if ($showUnhealthyHelper)
|
||||
@if ($showUnknownHelper)
|
||||
<div class="px-2">
|
||||
<x-helper
|
||||
helper="Unhealthy state. <span class='dark:text-warning text-coollabs'>This doesn't mean that the resource is malfunctioning.</span><br><br>- If the resource is accessible, it indicates that no health check is configured - it is not mandatory.<br>- If the resource is not accessible (returning 404 or 503), it may indicate that a health check is needed and has not passed. <span class='dark:text-warning text-coollabs'>Your action is required.</span><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>.">
|
||||
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">
|
||||
@@ -36,6 +62,22 @@
|
||||
</svg>
|
||||
</x-slot:icon>
|
||||
</x-helper>
|
||||
</div>
|
||||
@endif
|
||||
@if ($showUnhealthyHelper)
|
||||
<div class="px-2">
|
||||
<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-slot:icon>
|
||||
</x-helper>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
@if (str($complexStatus)->contains('running'))
|
||||
<x-status.running :status="$complexStatus" />
|
||||
@elseif(str($complexStatus)->contains('starting'))
|
||||
<x-status.restarting :status="$complexStatus" />
|
||||
@elseif(str($complexStatus)->contains('restarting'))
|
||||
<x-status.restarting :status="$complexStatus" />
|
||||
@elseif(str($complexStatus)->contains('degraded'))
|
||||
<x-status.degraded :status="$complexStatus" />
|
||||
@php
|
||||
$displayStatus = formatContainerStatus($complexStatus);
|
||||
@endphp
|
||||
@if (str($displayStatus)->lower()->contains('running'))
|
||||
<x-status.running :status="$displayStatus" />
|
||||
@elseif(str($displayStatus)->lower()->contains('starting'))
|
||||
<x-status.restarting :status="$displayStatus" />
|
||||
@elseif(str($displayStatus)->lower()->contains('restarting'))
|
||||
<x-status.restarting :status="$displayStatus" />
|
||||
@elseif(str($displayStatus)->lower()->contains('degraded'))
|
||||
<x-status.degraded :status="$displayStatus" />
|
||||
@else
|
||||
<x-status.stopped :status="$complexStatus" />
|
||||
<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'
|
||||
|
||||
@@ -2,12 +2,47 @@
|
||||
'status' => 'Stopped',
|
||||
'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;
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
} else {
|
||||
// Simple status without health
|
||||
$displayStatus = str($status)->headline();
|
||||
$healthStatus = null;
|
||||
}
|
||||
@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">{{ str($status)->before(':')->headline() }}</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>
|
||||
|
||||
62
resources/views/emails/traefik-version-outdated.blade.php
Normal file
62
resources/views/emails/traefik-version-outdated.blade.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<x-emails.layout>
|
||||
{{ $count }} server(s) are running outdated Traefik proxy. Update recommended for security and features.
|
||||
|
||||
## Affected Servers
|
||||
|
||||
@foreach ($servers as $server)
|
||||
@php
|
||||
$info = $server->outdatedInfo ?? [];
|
||||
$current = $info['current'] ?? 'unknown';
|
||||
$latest = $info['latest'] ?? 'unknown';
|
||||
$isPatch = ($info['type'] ?? 'patch_update') === 'patch_update';
|
||||
$hasNewerBranch = isset($info['newer_branch_target']);
|
||||
$hasUpgrades = $hasUpgrades ?? false;
|
||||
if (!$isPatch || $hasNewerBranch) {
|
||||
$hasUpgrades = true;
|
||||
}
|
||||
// Add 'v' prefix for display
|
||||
$current = str_starts_with($current, 'v') ? $current : "v{$current}";
|
||||
$latest = str_starts_with($latest, 'v') ? $latest : "v{$latest}";
|
||||
|
||||
// For minor upgrades, use the upgrade_target (e.g., "v3.6")
|
||||
if (!$isPatch && isset($info['upgrade_target'])) {
|
||||
$upgradeTarget = str_starts_with($info['upgrade_target'], 'v') ? $info['upgrade_target'] : "v{$info['upgrade_target']}";
|
||||
} else {
|
||||
// For patch updates, show the full version
|
||||
$upgradeTarget = $latest;
|
||||
}
|
||||
|
||||
// Get newer branch info if available
|
||||
if ($hasNewerBranch) {
|
||||
$newerBranchTarget = $info['newer_branch_target'];
|
||||
$newerBranchLatest = str_starts_with($info['newer_branch_latest'], 'v') ? $info['newer_branch_latest'] : "v{$info['newer_branch_latest']}";
|
||||
}
|
||||
@endphp
|
||||
@if ($isPatch && $hasNewerBranch)
|
||||
- **{{ $server->name }}**: {{ $current }} → {{ $upgradeTarget }} (patch update available) | Also available: {{ $newerBranchTarget }} (latest patch: {{ $newerBranchLatest }}) - new minor version
|
||||
@elseif ($isPatch)
|
||||
- **{{ $server->name }}**: {{ $current }} → {{ $upgradeTarget }} (patch update available)
|
||||
@else
|
||||
- **{{ $server->name }}**: {{ $current }} (latest patch: {{ $latest }}) → {{ $upgradeTarget }} (new minor version available)
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
## Recommendation
|
||||
|
||||
It is recommended to test the new Traefik version before switching it in production environments. You can update your proxy configuration through your [Coolify Dashboard]({{ config('app.url') }}).
|
||||
|
||||
@if ($hasUpgrades ?? false)
|
||||
**Important for minor version upgrades:** Before upgrading to a new minor version, please read the [Traefik changelog](https://github.com/traefik/traefik/releases) to understand breaking changes and new features.
|
||||
@endif
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Review the [Traefik release notes](https://github.com/traefik/traefik/releases) for changes
|
||||
2. Test the new version in a non-production environment
|
||||
3. Update your proxy configuration when ready
|
||||
4. Monitor services after the update
|
||||
|
||||
---
|
||||
|
||||
You can manage your server proxy settings in your Coolify Dashboard.
|
||||
</x-emails.layout>
|
||||
@@ -17,7 +17,8 @@
|
||||
localStorage.setItem('pageWidth', 'full');
|
||||
}
|
||||
}
|
||||
}" x-cloak class="mx-auto" :class="pageWidth === 'full' ? '' : 'max-w-7xl'">
|
||||
}" x-cloak class="mx-auto dark:text-inherit text-black"
|
||||
:class="pageWidth === 'full' ? '' : 'max-w-7xl'">
|
||||
<div class="relative z-50 lg:hidden" :class="open ? 'block' : 'hidden'" role="dialog" aria-modal="true">
|
||||
<div class="fixed inset-0 bg-black/80" x-on:click="open = false"></div>
|
||||
<div class="fixed inset-y-0 right-0 h-full flex">
|
||||
@@ -45,9 +46,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sticky top-0 z-40 flex items-center justify-between px-4 py-4 gap-x-6 sm:px-6 lg:hidden bg-white/95 dark:bg-base/95 backdrop-blur-sm border-b border-neutral-300/50 dark:border-coolgray-200/50">
|
||||
<div
|
||||
class="sticky top-0 z-40 flex items-center justify-between px-4 py-4 gap-x-6 sm:px-6 lg:hidden bg-white/95 dark:bg-base/95 backdrop-blur-sm border-b border-neutral-300/50 dark:border-coolgray-200/50">
|
||||
<div class="flex items-center gap-3 flex-shrink-0">
|
||||
<a href="/" class="text-xl font-bold tracking-wide dark:text-white hover:opacity-80 transition-opacity">Coolify</a>
|
||||
<a href="/"
|
||||
class="text-xl font-bold tracking-wide dark:text-white hover:opacity-80 transition-opacity">Coolify</a>
|
||||
<livewire:switch-team />
|
||||
</div>
|
||||
<button type="button" class="-m-2.5 p-2.5 dark:text-warning" x-on:click="open = !open">
|
||||
|
||||
@@ -2,17 +2,19 @@
|
||||
<html data-theme="dark" lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<script>
|
||||
// Immediate theme application - runs before any rendering
|
||||
(function() {
|
||||
(function () {
|
||||
const t = localStorage.theme || 'dark';
|
||||
const d = t === 'dark' || (t === 'system' && matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
document.documentElement.classList[d ? 'add' : 'remove']('dark');
|
||||
document.documentElement.setAttribute('data-theme', d ? 'dark' : 'light');
|
||||
})();
|
||||
</script>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex">
|
||||
<meta name="theme-color" content="#101010" id="theme-color-meta" />
|
||||
<meta name="theme-color" content="#ffffff" id="theme-color-meta" />
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
<meta name="Description" content="Coolify: An open-source & self-hostable Heroku / Netlify / Vercel alternative" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
@@ -73,102 +75,102 @@
|
||||
</head>
|
||||
@section('body')
|
||||
|
||||
<body>
|
||||
<x-toast />
|
||||
<script data-navigate-once>
|
||||
// Global HTML sanitization function using DOMPurify
|
||||
window.sanitizeHTML = function(html) {
|
||||
if (!html) return '';
|
||||
const URL_RE = /^(https?:|mailto:)/i;
|
||||
const config = {
|
||||
ALLOWED_TAGS: ['a', 'b', 'br', 'code', 'del', 'div', 'em', 'i', 'p', 'pre', 's', 'span', 'strong',
|
||||
'u'
|
||||
],
|
||||
ALLOWED_ATTR: ['class', 'href', 'target', 'title', 'rel'],
|
||||
ALLOW_DATA_ATTR: false,
|
||||
FORBID_TAGS: ['script', 'object', 'embed', 'applet', 'iframe', 'form', 'input', 'button', 'select',
|
||||
'textarea', 'details', 'summary', 'dialog', 'style'
|
||||
],
|
||||
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur', 'onchange',
|
||||
'onsubmit', 'ontoggle', 'style'
|
||||
],
|
||||
KEEP_CONTENT: true,
|
||||
RETURN_DOM: false,
|
||||
RETURN_DOM_FRAGMENT: false,
|
||||
SANITIZE_DOM: true,
|
||||
SANITIZE_NAMED_PROPS: true,
|
||||
SAFE_FOR_TEMPLATES: true,
|
||||
ALLOWED_URI_REGEXP: URL_RE
|
||||
};
|
||||
|
||||
// One-time hook registration (idempotent pattern)
|
||||
if (!window.__dpLinkHook) {
|
||||
DOMPurify.addHook('afterSanitizeAttributes', node => {
|
||||
// Remove Alpine.js directives to prevent XSS
|
||||
if (node.hasAttributes && node.hasAttributes()) {
|
||||
const attrs = Array.from(node.attributes);
|
||||
attrs.forEach(attr => {
|
||||
// Remove x-* attributes (Alpine directives)
|
||||
if (attr.name.startsWith('x-')) {
|
||||
node.removeAttribute(attr.name);
|
||||
}
|
||||
// Remove @* attributes (Alpine event shorthand)
|
||||
if (attr.name.startsWith('@')) {
|
||||
node.removeAttribute(attr.name);
|
||||
}
|
||||
// Remove :* attributes (Alpine binding shorthand)
|
||||
if (attr.name.startsWith(':')) {
|
||||
node.removeAttribute(attr.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Existing link sanitization
|
||||
if (node.nodeName === 'A' && node.hasAttribute('href')) {
|
||||
const href = node.getAttribute('href') || '';
|
||||
if (!URL_RE.test(href)) node.removeAttribute('href');
|
||||
if (node.getAttribute('target') === '_blank') {
|
||||
node.setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
}
|
||||
});
|
||||
window.__dpLinkHook = true;
|
||||
}
|
||||
return DOMPurify.sanitize(html, config);
|
||||
<body class="dark:text-inherit text-black">
|
||||
<x-toast />
|
||||
<script data-navigate-once>
|
||||
// Global HTML sanitization function using DOMPurify
|
||||
window.sanitizeHTML = function (html) {
|
||||
if (!html) return '';
|
||||
const URL_RE = /^(https?:|mailto:)/i;
|
||||
const config = {
|
||||
ALLOWED_TAGS: ['a', 'b', 'br', 'code', 'del', 'div', 'em', 'i', 'p', 'pre', 's', 'span', 'strong',
|
||||
'u'
|
||||
],
|
||||
ALLOWED_ATTR: ['class', 'href', 'target', 'title', 'rel'],
|
||||
ALLOW_DATA_ATTR: false,
|
||||
FORBID_TAGS: ['script', 'object', 'embed', 'applet', 'iframe', 'form', 'input', 'button', 'select',
|
||||
'textarea', 'details', 'summary', 'dialog', 'style'
|
||||
],
|
||||
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur', 'onchange',
|
||||
'onsubmit', 'ontoggle', 'style'
|
||||
],
|
||||
KEEP_CONTENT: true,
|
||||
RETURN_DOM: false,
|
||||
RETURN_DOM_FRAGMENT: false,
|
||||
SANITIZE_DOM: true,
|
||||
SANITIZE_NAMED_PROPS: true,
|
||||
SAFE_FOR_TEMPLATES: true,
|
||||
ALLOWED_URI_REGEXP: URL_RE
|
||||
};
|
||||
|
||||
// Initialize theme if not set
|
||||
if (!('theme' in localStorage)) {
|
||||
localStorage.theme = 'dark';
|
||||
}
|
||||
// One-time hook registration (idempotent pattern)
|
||||
if (!window.__dpLinkHook) {
|
||||
DOMPurify.addHook('afterSanitizeAttributes', node => {
|
||||
// Remove Alpine.js directives to prevent XSS
|
||||
if (node.hasAttributes && node.hasAttributes()) {
|
||||
const attrs = Array.from(node.attributes);
|
||||
attrs.forEach(attr => {
|
||||
// Remove x-* attributes (Alpine directives)
|
||||
if (attr.name.startsWith('x-')) {
|
||||
node.removeAttribute(attr.name);
|
||||
}
|
||||
// Remove @* attributes (Alpine event shorthand)
|
||||
if (attr.name.startsWith('@')) {
|
||||
node.removeAttribute(attr.name);
|
||||
}
|
||||
// Remove :* attributes (Alpine binding shorthand)
|
||||
if (attr.name.startsWith(':')) {
|
||||
node.removeAttribute(attr.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let theme = localStorage.theme
|
||||
let cpuColor = '#1e90ff'
|
||||
let ramColor = '#00ced1'
|
||||
let textColor = '#ffffff'
|
||||
let editorBackground = '#181818'
|
||||
let editorTheme = 'blackboard'
|
||||
|
||||
function checkTheme() {
|
||||
theme = localStorage.theme
|
||||
if (theme == 'system') {
|
||||
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
}
|
||||
if (theme == 'dark') {
|
||||
cpuColor = '#1e90ff'
|
||||
ramColor = '#00ced1'
|
||||
textColor = '#ffffff'
|
||||
editorBackground = '#181818'
|
||||
editorTheme = 'blackboard'
|
||||
} else {
|
||||
cpuColor = '#1e90ff'
|
||||
ramColor = '#00ced1'
|
||||
textColor = '#000000'
|
||||
editorBackground = '#ffffff'
|
||||
editorTheme = null
|
||||
}
|
||||
// Existing link sanitization
|
||||
if (node.nodeName === 'A' && node.hasAttribute('href')) {
|
||||
const href = node.getAttribute('href') || '';
|
||||
if (!URL_RE.test(href)) node.removeAttribute('href');
|
||||
if (node.getAttribute('target') === '_blank') {
|
||||
node.setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
}
|
||||
});
|
||||
window.__dpLinkHook = true;
|
||||
}
|
||||
@auth
|
||||
return DOMPurify.sanitize(html, config);
|
||||
};
|
||||
|
||||
// Initialize theme if not set
|
||||
if (!('theme' in localStorage)) {
|
||||
localStorage.theme = 'dark';
|
||||
}
|
||||
|
||||
let theme = localStorage.theme
|
||||
let cpuColor = '#1e90ff'
|
||||
let ramColor = '#00ced1'
|
||||
let textColor = '#ffffff'
|
||||
let editorBackground = '#181818'
|
||||
let editorTheme = 'blackboard'
|
||||
|
||||
function checkTheme() {
|
||||
theme = localStorage.theme
|
||||
if (theme == 'system') {
|
||||
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
}
|
||||
if (theme == 'dark') {
|
||||
cpuColor = '#1e90ff'
|
||||
ramColor = '#00ced1'
|
||||
textColor = '#ffffff'
|
||||
editorBackground = '#181818'
|
||||
editorTheme = 'blackboard'
|
||||
} else {
|
||||
cpuColor = '#1e90ff'
|
||||
ramColor = '#00ced1'
|
||||
textColor = '#000000'
|
||||
editorBackground = '#ffffff'
|
||||
editorTheme = null
|
||||
}
|
||||
}
|
||||
@auth
|
||||
window.Pusher = Pusher;
|
||||
window.Echo = new Echo({
|
||||
broadcaster: 'pusher',
|
||||
@@ -197,131 +199,131 @@
|
||||
// Maximum number of reconnection attempts
|
||||
maxAttempts: 15
|
||||
});
|
||||
@endauth
|
||||
let checkHealthInterval = null;
|
||||
let checkIfIamDeadInterval = null;
|
||||
@endauth
|
||||
let checkHealthInterval = null;
|
||||
let checkIfIamDeadInterval = null;
|
||||
|
||||
function changePasswordFieldType(event) {
|
||||
let element = event.target
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if (element.className === "relative") {
|
||||
break;
|
||||
}
|
||||
element = element.parentElement;
|
||||
function changePasswordFieldType(event) {
|
||||
let element = event.target
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if (element.className === "relative") {
|
||||
break;
|
||||
}
|
||||
element = element.children[1];
|
||||
if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
|
||||
if (element.type === 'password') {
|
||||
element.type = 'text';
|
||||
if (element.disabled) return;
|
||||
element.classList.add('truncate');
|
||||
this.type = 'text';
|
||||
} else {
|
||||
element.type = 'password';
|
||||
if (element.disabled) return;
|
||||
element.classList.remove('truncate');
|
||||
this.type = 'password';
|
||||
}
|
||||
element = element.parentElement;
|
||||
}
|
||||
element = element.children[1];
|
||||
if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
|
||||
if (element.type === 'password') {
|
||||
element.type = 'text';
|
||||
if (element.disabled) return;
|
||||
element.classList.add('truncate');
|
||||
this.type = 'text';
|
||||
} else {
|
||||
element.type = 'password';
|
||||
if (element.disabled) return;
|
||||
element.classList.remove('truncate');
|
||||
this.type = 'password';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard(text) {
|
||||
navigator?.clipboard?.writeText(text) && window.Livewire.dispatch('success', 'Copied to clipboard.');
|
||||
}
|
||||
document.addEventListener('livewire:init', () => {
|
||||
window.Livewire.on('reloadWindow', (timeout) => {
|
||||
if (timeout) {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, timeout);
|
||||
return;
|
||||
} else {
|
||||
function copyToClipboard(text) {
|
||||
navigator?.clipboard?.writeText(text) && window.Livewire.dispatch('success', 'Copied to clipboard.');
|
||||
}
|
||||
document.addEventListener('livewire:init', () => {
|
||||
window.Livewire.on('reloadWindow', (timeout) => {
|
||||
if (timeout) {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}
|
||||
})
|
||||
window.Livewire.on('info', (message) => {
|
||||
if (typeof message === 'string') {
|
||||
window.toast('Info', {
|
||||
type: 'info',
|
||||
description: message,
|
||||
})
|
||||
return;
|
||||
}
|
||||
if (message.length == 1) {
|
||||
window.toast('Info', {
|
||||
type: 'info',
|
||||
description: message[0],
|
||||
})
|
||||
} else if (message.length == 2) {
|
||||
window.toast(message[0], {
|
||||
type: 'info',
|
||||
description: message[1],
|
||||
})
|
||||
}
|
||||
})
|
||||
window.Livewire.on('error', (message) => {
|
||||
if (typeof message === 'string') {
|
||||
window.toast('Error', {
|
||||
type: 'danger',
|
||||
description: message,
|
||||
})
|
||||
return;
|
||||
}
|
||||
if (message.length == 1) {
|
||||
window.toast('Error', {
|
||||
type: 'danger',
|
||||
description: message[0],
|
||||
})
|
||||
} else if (message.length == 2) {
|
||||
window.toast(message[0], {
|
||||
type: 'danger',
|
||||
description: message[1],
|
||||
})
|
||||
}
|
||||
})
|
||||
window.Livewire.on('warning', (message) => {
|
||||
if (typeof message === 'string') {
|
||||
window.toast('Warning', {
|
||||
type: 'warning',
|
||||
description: message,
|
||||
})
|
||||
return;
|
||||
}
|
||||
if (message.length == 1) {
|
||||
window.toast('Warning', {
|
||||
type: 'warning',
|
||||
description: message[0],
|
||||
})
|
||||
} else if (message.length == 2) {
|
||||
window.toast(message[0], {
|
||||
type: 'warning',
|
||||
description: message[1],
|
||||
})
|
||||
}
|
||||
})
|
||||
window.Livewire.on('success', (message) => {
|
||||
if (typeof message === 'string') {
|
||||
window.toast('Success', {
|
||||
type: 'success',
|
||||
description: message,
|
||||
})
|
||||
return;
|
||||
}
|
||||
if (message.length == 1) {
|
||||
window.toast('Success', {
|
||||
type: 'success',
|
||||
description: message[0],
|
||||
})
|
||||
} else if (message.length == 2) {
|
||||
window.toast(message[0], {
|
||||
type: 'success',
|
||||
description: message[1],
|
||||
})
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
}, timeout);
|
||||
return;
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
})
|
||||
window.Livewire.on('info', (message) => {
|
||||
if (typeof message === 'string') {
|
||||
window.toast('Info', {
|
||||
type: 'info',
|
||||
description: message,
|
||||
})
|
||||
return;
|
||||
}
|
||||
if (message.length == 1) {
|
||||
window.toast('Info', {
|
||||
type: 'info',
|
||||
description: message[0],
|
||||
})
|
||||
} else if (message.length == 2) {
|
||||
window.toast(message[0], {
|
||||
type: 'info',
|
||||
description: message[1],
|
||||
})
|
||||
}
|
||||
})
|
||||
window.Livewire.on('error', (message) => {
|
||||
if (typeof message === 'string') {
|
||||
window.toast('Error', {
|
||||
type: 'danger',
|
||||
description: message,
|
||||
})
|
||||
return;
|
||||
}
|
||||
if (message.length == 1) {
|
||||
window.toast('Error', {
|
||||
type: 'danger',
|
||||
description: message[0],
|
||||
})
|
||||
} else if (message.length == 2) {
|
||||
window.toast(message[0], {
|
||||
type: 'danger',
|
||||
description: message[1],
|
||||
})
|
||||
}
|
||||
})
|
||||
window.Livewire.on('warning', (message) => {
|
||||
if (typeof message === 'string') {
|
||||
window.toast('Warning', {
|
||||
type: 'warning',
|
||||
description: message,
|
||||
})
|
||||
return;
|
||||
}
|
||||
if (message.length == 1) {
|
||||
window.toast('Warning', {
|
||||
type: 'warning',
|
||||
description: message[0],
|
||||
})
|
||||
} else if (message.length == 2) {
|
||||
window.toast(message[0], {
|
||||
type: 'warning',
|
||||
description: message[1],
|
||||
})
|
||||
}
|
||||
})
|
||||
window.Livewire.on('success', (message) => {
|
||||
if (typeof message === 'string') {
|
||||
window.toast('Success', {
|
||||
type: 'success',
|
||||
description: message,
|
||||
})
|
||||
return;
|
||||
}
|
||||
if (message.length == 1) {
|
||||
window.toast('Success', {
|
||||
type: 'success',
|
||||
description: message[0],
|
||||
})
|
||||
} else if (message.length == 2) {
|
||||
window.toast(message[0], {
|
||||
type: 'success',
|
||||
description: message[1],
|
||||
})
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
@show
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -5,7 +5,7 @@
|
||||
])>
|
||||
@if ($activity)
|
||||
@if (isset($header))
|
||||
<div class="flex gap-2 pb-2 flex-shrink-0">
|
||||
<div class="flex gap-2 pb-2 flex-shrink-0" @if ($isPollingActive) wire:poll.1000ms @endif>
|
||||
<h3>{{ $header }}</h3>
|
||||
@if ($isPollingActive)
|
||||
<x-loading />
|
||||
|
||||
@@ -546,6 +546,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($prerequisiteInstallAttempts > 0)
|
||||
<div class="p-6 bg-neutral-50 dark:bg-coolgray-200 rounded-lg border border-neutral-200 dark:border-coolgray-400">
|
||||
<h3 class="font-bold text-black dark:text-white mb-4">Installing Prerequisites</h3>
|
||||
<livewire:activity-monitor header="Prerequisites Installation Logs" :showWaiting="false" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<x-slide-over closeWithX fullScreen>
|
||||
<x-slot:title>Server Validation</x-slot:title>
|
||||
<x-slot:content>
|
||||
|
||||
@@ -254,7 +254,8 @@
|
||||
class="fixed top-0 left-0 z-99 flex items-start justify-center w-screen h-screen pt-[10vh]">
|
||||
<div @click="closeModal()" class="absolute inset-0 w-full h-full bg-black/50 backdrop-blur-sm">
|
||||
</div>
|
||||
<div x-show="modalOpen" x-trap.inert="modalOpen" x-init="$watch('modalOpen', value => { document.body.style.overflow = value ? 'hidden' : '' })"
|
||||
<div x-show="modalOpen" x-trap.inert="modalOpen"
|
||||
x-init="$watch('modalOpen', value => { document.body.style.overflow = value ? 'hidden' : '' })"
|
||||
x-transition:enter="ease-out duration-200" x-transition:enter-start="opacity-0 -translate-y-4 scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 scale-100" x-transition:leave="ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 scale-100"
|
||||
@@ -271,8 +272,7 @@
|
||||
</svg>
|
||||
<svg x-show="isLoadingInitialData" x-cloak class="animate-spin h-5 w-5 text-warning"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
|
||||
stroke-width="4">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4">
|
||||
</circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||
@@ -311,8 +311,8 @@
|
||||
class="text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<div>
|
||||
@@ -327,13 +327,11 @@
|
||||
</div>
|
||||
</div>
|
||||
@if ($loadingServers)
|
||||
<div
|
||||
class="flex items-center gap-3 p-3 bg-neutral-50 dark:bg-coolgray-200 rounded-lg">
|
||||
<svg class="animate-spin h-5 w-5 text-yellow-500"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10"
|
||||
stroke="currentColor" stroke-width="4"></circle>
|
||||
<div class="flex items-center gap-3 p-3 bg-neutral-50 dark:bg-coolgray-200 rounded-lg">
|
||||
<svg class="animate-spin h-5 w-5 text-yellow-500" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
|
||||
stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||
</path>
|
||||
@@ -343,8 +341,7 @@
|
||||
</div>
|
||||
@elseif (count($availableServers) > 0)
|
||||
@foreach ($availableServers as $index => $server)
|
||||
<button type="button"
|
||||
wire:click="selectServer({{ $server['id'] }}, true)"
|
||||
<button type="button" wire:click="selectServer({{ $server['id'] }}, true)"
|
||||
class="search-result-item w-full text-left block px-4 py-3 min-h-[4rem] hover:bg-yellow-50 dark:hover:bg-yellow-900/20 transition-colors focus:outline-none focus:bg-yellow-100 dark:focus:bg-yellow-900/30 border-b border-neutral-100 dark:border-coolgray-300 last:border-0">
|
||||
<div class="flex items-center justify-between gap-3 min-h-[2.5rem]">
|
||||
<div class="flex-1 min-w-0">
|
||||
@@ -352,8 +349,7 @@
|
||||
{{ $server['name'] }}
|
||||
</div>
|
||||
@if (!empty($server['description']))
|
||||
<div
|
||||
class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
<div class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
{{ $server['description'] }}
|
||||
</div>
|
||||
@else
|
||||
@@ -363,10 +359,10 @@
|
||||
@endif
|
||||
</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="shrink-0 h-5 w-5 text-yellow-500 dark:text-yellow-400"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
class="shrink-0 h-5 w-5 text-yellow-500 dark:text-yellow-400" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
@@ -388,10 +384,10 @@
|
||||
<button type="button"
|
||||
@click="$wire.set('searchQuery', ''); setTimeout(() => $refs.searchInput.focus(), 100)"
|
||||
class="text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<div>
|
||||
@@ -406,13 +402,11 @@
|
||||
</div>
|
||||
</div>
|
||||
@if ($loadingDestinations)
|
||||
<div
|
||||
class="flex items-center gap-3 p-3 bg-neutral-50 dark:bg-coolgray-200 rounded-lg">
|
||||
<svg class="animate-spin h-5 w-5 text-yellow-500"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10"
|
||||
stroke="currentColor" stroke-width="4"></circle>
|
||||
<div class="flex items-center gap-3 p-3 bg-neutral-50 dark:bg-coolgray-200 rounded-lg">
|
||||
<svg class="animate-spin h-5 w-5 text-yellow-500" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
|
||||
stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||
</path>
|
||||
@@ -422,25 +416,22 @@
|
||||
</div>
|
||||
@elseif (count($availableDestinations) > 0)
|
||||
@foreach ($availableDestinations as $index => $destination)
|
||||
<button type="button"
|
||||
wire:click="selectDestination('{{ $destination['uuid'] }}', true)"
|
||||
<button type="button" wire:click="selectDestination('{{ $destination['uuid'] }}', true)"
|
||||
class="search-result-item w-full text-left block px-4 py-3 min-h-[4rem] hover:bg-yellow-50 dark:hover:bg-yellow-900/20 transition-colors focus:outline-none focus:bg-yellow-100 dark:focus:bg-yellow-900/30 border-b border-neutral-100 dark:border-coolgray-300 last:border-0">
|
||||
<div
|
||||
class="flex items-center justify-between gap-3 min-h-[2.5rem]">
|
||||
<div class="flex items-center justify-between gap-3 min-h-[2.5rem]">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-medium text-neutral-900 dark:text-white">
|
||||
{{ $destination['name'] }}
|
||||
</div>
|
||||
<div
|
||||
class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
<div class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
Network: {{ $destination['network'] }}
|
||||
</div>
|
||||
</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="shrink-0 h-5 w-5 text-yellow-500 dark:text-yellow-400"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
class="shrink-0 h-5 w-5 text-yellow-500 dark:text-yellow-400" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
@@ -462,10 +453,10 @@
|
||||
<button type="button"
|
||||
@click="$wire.set('searchQuery', ''); setTimeout(() => $refs.searchInput.focus(), 100)"
|
||||
class="text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<div>
|
||||
@@ -480,13 +471,11 @@
|
||||
</div>
|
||||
</div>
|
||||
@if ($loadingProjects)
|
||||
<div
|
||||
class="flex items-center gap-3 p-3 bg-neutral-50 dark:bg-coolgray-200 rounded-lg">
|
||||
<svg class="animate-spin h-5 w-5 text-yellow-500"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10"
|
||||
stroke="currentColor" stroke-width="4"></circle>
|
||||
<div class="flex items-center gap-3 p-3 bg-neutral-50 dark:bg-coolgray-200 rounded-lg">
|
||||
<svg class="animate-spin h-5 w-5 text-yellow-500" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
|
||||
stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||
</path>
|
||||
@@ -496,18 +485,15 @@
|
||||
</div>
|
||||
@elseif (count($availableProjects) > 0)
|
||||
@foreach ($availableProjects as $index => $project)
|
||||
<button type="button"
|
||||
wire:click="selectProject('{{ $project['uuid'] }}', true)"
|
||||
<button type="button" wire:click="selectProject('{{ $project['uuid'] }}', true)"
|
||||
class="search-result-item w-full text-left block px-4 py-3 min-h-[4rem] hover:bg-yellow-50 dark:hover:bg-yellow-900/20 transition-colors focus:outline-none focus:bg-yellow-100 dark:focus:bg-yellow-900/30 border-b border-neutral-100 dark:border-coolgray-300 last:border-0">
|
||||
<div
|
||||
class="flex items-center justify-between gap-3 min-h-[2.5rem]">
|
||||
<div class="flex items-center justify-between gap-3 min-h-[2.5rem]">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-medium text-neutral-900 dark:text-white">
|
||||
{{ $project['name'] }}
|
||||
</div>
|
||||
@if (!empty($project['description']))
|
||||
<div
|
||||
class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
<div class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
{{ $project['description'] }}
|
||||
</div>
|
||||
@else
|
||||
@@ -517,10 +503,10 @@
|
||||
@endif
|
||||
</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="shrink-0 h-5 w-5 text-yellow-500 dark:text-yellow-400"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
class="shrink-0 h-5 w-5 text-yellow-500 dark:text-yellow-400" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
@@ -542,10 +528,10 @@
|
||||
<button type="button"
|
||||
@click="$wire.set('searchQuery', ''); setTimeout(() => $refs.searchInput.focus(), 100)"
|
||||
class="text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<div>
|
||||
@@ -560,13 +546,11 @@
|
||||
</div>
|
||||
</div>
|
||||
@if ($loadingEnvironments)
|
||||
<div
|
||||
class="flex items-center gap-3 p-3 bg-neutral-50 dark:bg-coolgray-200 rounded-lg">
|
||||
<svg class="animate-spin h-5 w-5 text-yellow-500"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10"
|
||||
stroke="currentColor" stroke-width="4"></circle>
|
||||
<div class="flex items-center gap-3 p-3 bg-neutral-50 dark:bg-coolgray-200 rounded-lg">
|
||||
<svg class="animate-spin h-5 w-5 text-yellow-500" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
|
||||
stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||
</path>
|
||||
@@ -576,18 +560,15 @@
|
||||
</div>
|
||||
@elseif (count($availableEnvironments) > 0)
|
||||
@foreach ($availableEnvironments as $index => $environment)
|
||||
<button type="button"
|
||||
wire:click="selectEnvironment('{{ $environment['uuid'] }}', true)"
|
||||
<button type="button" wire:click="selectEnvironment('{{ $environment['uuid'] }}', true)"
|
||||
class="search-result-item w-full text-left block px-4 py-3 min-h-[4rem] hover:bg-yellow-50 dark:hover:bg-yellow-900/20 transition-colors focus:outline-none focus:bg-yellow-100 dark:focus:bg-yellow-900/30 border-b border-neutral-100 dark:border-coolgray-300 last:border-0">
|
||||
<div
|
||||
class="flex items-center justify-between gap-3 min-h-[2.5rem]">
|
||||
<div class="flex items-center justify-between gap-3 min-h-[2.5rem]">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-medium text-neutral-900 dark:text-white">
|
||||
{{ $environment['name'] }}
|
||||
</div>
|
||||
@if (!empty($environment['description']))
|
||||
<div
|
||||
class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
<div class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
{{ $environment['description'] }}
|
||||
</div>
|
||||
@else
|
||||
@@ -597,10 +578,10 @@
|
||||
@endif
|
||||
</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="shrink-0 h-5 w-5 text-yellow-500 dark:text-yellow-400"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
class="shrink-0 h-5 w-5 text-yellow-500 dark:text-yellow-400" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
@@ -639,8 +620,7 @@
|
||||
<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">
|
||||
<span
|
||||
class="font-medium text-neutral-900 dark:text-white truncate">
|
||||
<span class="font-medium text-neutral-900 dark:text-white truncate">
|
||||
{{ $result['name'] }}
|
||||
</span>
|
||||
<span
|
||||
@@ -661,15 +641,13 @@
|
||||
</span>
|
||||
</div>
|
||||
@if (!empty($result['project']) && !empty($result['environment']))
|
||||
<div
|
||||
class="text-xs text-neutral-500 dark:text-neutral-400 mb-1">
|
||||
<div class="text-xs text-neutral-500 dark:text-neutral-400 mb-1">
|
||||
{{ $result['project'] }} /
|
||||
{{ $result['environment'] }}
|
||||
</div>
|
||||
@endif
|
||||
@if (!empty($result['description']))
|
||||
<div
|
||||
class="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
<div class="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{{ Str::limit($result['description'], 80) }}
|
||||
</div>
|
||||
@endif
|
||||
@@ -677,8 +655,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="shrink-0 h-5 w-5 text-neutral-300 dark:text-neutral-600 self-center"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
@@ -708,16 +686,15 @@
|
||||
<div
|
||||
class="flex-shrink-0 w-10 h-10 rounded-lg bg-yellow-100 dark:bg-yellow-900/40 flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 text-yellow-600 dark:text-yellow-400"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
class="h-5 w-5 text-yellow-600 dark:text-yellow-400" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<div
|
||||
class="font-medium text-neutral-900 dark:text-white truncate">
|
||||
<div class="font-medium text-neutral-900 dark:text-white truncate">
|
||||
{{ $item['name'] }}
|
||||
</div>
|
||||
@if (isset($item['quickcommand']))
|
||||
@@ -725,8 +702,7 @@
|
||||
class="text-xs text-neutral-500 dark:text-neutral-400 shrink-0">{{ $item['quickcommand'] }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<div
|
||||
class="text-sm text-neutral-600 dark:text-neutral-400 truncate">
|
||||
<div class="text-sm text-neutral-600 dark:text-neutral-400 truncate">
|
||||
{{ $item['description'] }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -734,8 +710,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="shrink-0 h-5 w-5 text-yellow-500 dark:text-yellow-400 self-center"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
@@ -820,8 +796,7 @@
|
||||
class="flex-shrink-0 w-10 h-10 rounded-lg bg-yellow-100 dark:bg-yellow-900/40 flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 text-yellow-600 dark:text-yellow-400"
|
||||
fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
@@ -869,14 +844,6 @@
|
||||
<p class="mt-2 text-xs text-neutral-400 dark:text-neutral-500">
|
||||
💡 Tip: Search for service names like "wordpress", "postgres", or "redis"
|
||||
</p>
|
||||
<div class="mt-4">
|
||||
<a href="{{ route('onboarding') }}" class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-coollabs dark:bg-warning hover:bg-coollabs-100 dark:hover:bg-warning/90 rounded-lg transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
View Onboarding Guide
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -897,12 +864,10 @@
|
||||
if (firstInput) firstInput.focus();
|
||||
}, 200);
|
||||
}
|
||||
})"
|
||||
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="modalOpen" 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="modalOpen=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="modalOpen" 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="modalOpen=false"
|
||||
class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
|
||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
|
||||
@@ -915,8 +880,8 @@
|
||||
<h3 class="text-2xl font-bold">New Project</h3>
|
||||
<button @click="modalOpen=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">
|
||||
<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>
|
||||
@@ -939,12 +904,10 @@
|
||||
if (firstInput) firstInput.focus();
|
||||
}, 200);
|
||||
}
|
||||
})"
|
||||
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="modalOpen" 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="modalOpen=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="modalOpen" 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="modalOpen=false"
|
||||
class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
|
||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
|
||||
@@ -957,8 +920,8 @@
|
||||
<h3 class="text-2xl font-bold">New Server</h3>
|
||||
<button @click="modalOpen=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">
|
||||
<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>
|
||||
@@ -981,12 +944,10 @@
|
||||
if (firstInput) firstInput.focus();
|
||||
}, 200);
|
||||
}
|
||||
})"
|
||||
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="modalOpen" 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="modalOpen=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="modalOpen" 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="modalOpen=false"
|
||||
class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
|
||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
|
||||
@@ -999,8 +960,8 @@
|
||||
<h3 class="text-2xl font-bold">New Team</h3>
|
||||
<button @click="modalOpen=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">
|
||||
<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>
|
||||
@@ -1023,12 +984,10 @@
|
||||
if (firstInput) firstInput.focus();
|
||||
}, 200);
|
||||
}
|
||||
})"
|
||||
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="modalOpen" 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="modalOpen=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="modalOpen" 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="modalOpen=false"
|
||||
class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
|
||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
|
||||
@@ -1041,8 +1000,8 @@
|
||||
<h3 class="text-2xl font-bold">New S3 Storage</h3>
|
||||
<button @click="modalOpen=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">
|
||||
<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>
|
||||
@@ -1065,12 +1024,10 @@
|
||||
if (firstInput) firstInput.focus();
|
||||
}, 200);
|
||||
}
|
||||
})"
|
||||
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="modalOpen" 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="modalOpen=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="modalOpen" 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="modalOpen=false"
|
||||
class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
|
||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
|
||||
@@ -1083,8 +1040,8 @@
|
||||
<h3 class="text-2xl font-bold">New Private Key</h3>
|
||||
<button @click="modalOpen=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">
|
||||
<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>
|
||||
@@ -1107,12 +1064,10 @@
|
||||
if (firstInput) firstInput.focus();
|
||||
}, 200);
|
||||
}
|
||||
})"
|
||||
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="modalOpen" 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="modalOpen=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="modalOpen" 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="modalOpen=false"
|
||||
class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
|
||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
|
||||
@@ -1125,8 +1080,8 @@
|
||||
<h3 class="text-2xl font-bold">New GitHub App</h3>
|
||||
<button @click="modalOpen=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">
|
||||
<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>
|
||||
@@ -1139,4 +1094,4 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,6 +80,8 @@
|
||||
label="Server Unreachable" />
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="serverPatchDiscordNotifications"
|
||||
label="Server Patching" />
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="traefikOutdatedDiscordNotifications"
|
||||
label="Traefik Proxy Outdated" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -161,6 +161,8 @@
|
||||
label="Server Unreachable" />
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="serverPatchEmailNotifications"
|
||||
label="Server Patching" />
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="traefikOutdatedEmailNotifications"
|
||||
label="Traefik Proxy Outdated" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -82,6 +82,8 @@
|
||||
label="Server Unreachable" />
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="serverPatchPushoverNotifications"
|
||||
label="Server Patching" />
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="traefikOutdatedPushoverNotifications"
|
||||
label="Traefik Proxy Outdated" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="serverUnreachableSlackNotifications"
|
||||
label="Server Unreachable" />
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="serverPatchSlackNotifications" label="Server Patching" />
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="traefikOutdatedSlackNotifications" label="Traefik Proxy Outdated" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -169,6 +169,15 @@
|
||||
<x-forms.input canGate="update" :canResource="$settings" type="password" placeholder="Custom Telegram Thread ID"
|
||||
id="telegramNotificationsServerPatchThreadId" />
|
||||
</div>
|
||||
|
||||
<div class="pl-1 flex gap-2">
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel" id="traefikOutdatedTelegramNotifications"
|
||||
label="Traefik Proxy Outdated" />
|
||||
</div>
|
||||
<x-forms.input canGate="update" :canResource="$settings" type="password" placeholder="Custom Telegram Thread ID"
|
||||
id="telegramNotificationsTraefikOutdatedThreadId" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
id="serverUnreachableWebhookNotifications" label="Server Unreachable" />
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel"
|
||||
id="serverPatchWebhookNotifications" label="Server Patching" />
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings" instantSave="saveModel"
|
||||
id="traefikOutdatedWebhookNotifications" label="Traefik Proxy Outdated" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,15 +30,15 @@
|
||||
@endif
|
||||
<a class="menu-item flex items-center gap-2" wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.servers', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Servers
|
||||
@if (str($application->status)->contains('degraded'))
|
||||
<span title="Some servers are unavailable">
|
||||
@if ($application->server_status == false)
|
||||
<span title="One or more servers are unreachable or misconfigured.">
|
||||
<svg class="w-4 h-4 text-error" 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" />
|
||||
</svg>
|
||||
</span>
|
||||
@elseif ($application->server_status == false)
|
||||
<span title="The underlying server(s) has problems.">
|
||||
@elseif ($application->additional_servers()->exists() && str($application->status)->contains('degraded'))
|
||||
<span title="Application is in degraded state across multiple servers.">
|
||||
<svg class="w-4 h-4 text-error" 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" />
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
<div>{{ $application->compose_parsing_version }}</div>
|
||||
@endif
|
||||
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
|
||||
@if ($application->build_pack === 'dockercompose')
|
||||
<x-forms.button canGate="update" :canResource="$application" wire:target='initLoadingCompose'
|
||||
x-on:click="$wire.dispatch('loadCompose', false)">
|
||||
{{ $application->docker_compose_raw ? 'Reload Compose File' : 'Load Compose File' }}
|
||||
</x-forms.button>
|
||||
@endif
|
||||
</div>
|
||||
<div>General configuration for your application.</div>
|
||||
<div class="flex flex-col gap-2 py-4">
|
||||
@@ -23,16 +29,15 @@
|
||||
@if (!$application->dockerfile && $application->build_pack !== 'dockerimage')
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2">
|
||||
<x-forms.select x-bind:disabled="shouldDisable()" wire:model.live="build_pack"
|
||||
label="Build Pack" required>
|
||||
<x-forms.select x-bind:disabled="shouldDisable()" wire:model.live="buildPack" label="Build Pack"
|
||||
required>
|
||||
<option value="nixpacks">Nixpacks</option>
|
||||
<option value="static">Static</option>
|
||||
<option value="dockerfile">Dockerfile</option>
|
||||
<option value="dockercompose">Docker Compose</option>
|
||||
</x-forms.select>
|
||||
@if ($application->settings->is_static || $application->build_pack === 'static')
|
||||
<x-forms.select x-bind:disabled="!canUpdate" id="static_image"
|
||||
label="Static Image" required>
|
||||
<x-forms.select x-bind:disabled="!canUpdate" id="staticImage" label="Static Image" required>
|
||||
<option value="nginx:alpine">nginx:alpine</option>
|
||||
<option disabled value="apache:alpine">apache:alpine</option>
|
||||
</x-forms.select>
|
||||
@@ -41,9 +46,10 @@
|
||||
|
||||
@if ($application->build_pack === 'dockercompose')
|
||||
@if (
|
||||
!is_null($parsedServices) &&
|
||||
!is_null($parsedServices) &&
|
||||
count($parsedServices) > 0 &&
|
||||
!$application->settings->is_raw_compose_deployment_enabled)
|
||||
!$application->settings->is_raw_compose_deployment_enabled
|
||||
)
|
||||
<h3 class="pt-6">Domains</h3>
|
||||
@foreach (data_get($parsedServices, 'services') as $serviceName => $service)
|
||||
@if (!isDatabaseImage(data_get($service, 'image')))
|
||||
@@ -66,37 +72,41 @@
|
||||
</div>
|
||||
@endif
|
||||
@if ($application->settings->is_static || $application->build_pack === 'static')
|
||||
<x-forms.textarea id="custom_nginx_configuration"
|
||||
<x-forms.textarea id="customNginxConfiguration"
|
||||
placeholder="Empty means default configuration will be used." label="Custom Nginx Configuration"
|
||||
helper="You can add custom Nginx configuration here." x-bind:disabled="!canUpdate" />
|
||||
@can('update', $application)
|
||||
<x-forms.button wire:click="generateNginxConfiguration">
|
||||
Generate Default Nginx Configuration
|
||||
</x-forms.button>
|
||||
<x-modal-confirmation title="Confirm Nginx Configuration Generation?"
|
||||
buttonTitle="Generate Default Nginx Configuration" buttonFullWidth
|
||||
submitAction="generateNginxConfiguration('{{ $application->settings->is_spa ? 'spa' : 'static' }}')"
|
||||
:actions="[
|
||||
'This will overwrite your current custom Nginx configuration.',
|
||||
'The default configuration will be generated based on your application type (' .
|
||||
($application->settings->is_spa ? 'SPA' : 'static') .
|
||||
').',
|
||||
]" />
|
||||
@endcan
|
||||
@endif
|
||||
<div class="w-96 pb-6">
|
||||
@if ($application->could_set_build_commands())
|
||||
<x-forms.checkbox instantSave id="is_static" label="Is it a static site?"
|
||||
<x-forms.checkbox instantSave id="isStatic" label="Is it a static site?"
|
||||
helper="If your application is a static site or the final build assets should be served as a static site, enable this."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
@if ($application->settings->is_static && $application->build_pack !== 'static')
|
||||
<x-forms.checkbox label="Is it a SPA (Single Page Application)?"
|
||||
helper="If your application is a SPA, enable this." id="is_spa" instantSave
|
||||
helper="If your application is a SPA, enable this." id="isSpa" instantSave
|
||||
x-bind:disabled="!canUpdate"></x-forms.checkbox>
|
||||
@endif
|
||||
</div>
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
<div class="flex items-end gap-2">
|
||||
@if ($application->settings->is_container_label_readonly_enabled == false)
|
||||
<x-forms.input placeholder="https://coolify.io" wire:model="fqdn"
|
||||
label="Domains" readonly
|
||||
<x-forms.input placeholder="https://coolify.io" wire:model="fqdn" label="Domains" readonly
|
||||
helper="Readonly labels are disabled. You can set the domains in the labels section."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@else
|
||||
<x-forms.input placeholder="https://coolify.io" wire:model="fqdn"
|
||||
label="Domains"
|
||||
<x-forms.input placeholder="https://coolify.io" wire:model="fqdn" label="Domains"
|
||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@can('update', $application)
|
||||
@@ -156,44 +166,42 @@
|
||||
</div>
|
||||
@if ($application->destination->server->isSwarm())
|
||||
@if ($application->build_pack !== 'dockerimage')
|
||||
<div>Docker Swarm requires the image to be available in a registry. More info <a
|
||||
class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
||||
target="_blank">here</a>.</div>
|
||||
<div>Docker Swarm requires the image to be available in a registry. More info <a class="underline"
|
||||
href="https://coolify.io/docs/knowledge-base/docker/registry" target="_blank">here</a>.</div>
|
||||
@endif
|
||||
@endif
|
||||
<div class="flex flex-col gap-2 xl:flex-row">
|
||||
@if ($application->build_pack === 'dockerimage')
|
||||
@if ($application->destination->server->isSwarm())
|
||||
<x-forms.input required id="docker_registry_image_name" label="Docker Image"
|
||||
<x-forms.input required id="dockerRegistryImageName" label="Docker Image"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input id="docker_registry_image_tag" label="Docker Image Tag or Hash"
|
||||
<x-forms.input id="dockerRegistryImageTag" label="Docker Image Tag or Hash"
|
||||
helper="Enter a tag (e.g., 'latest', 'v1.2.3') or SHA256 hash (e.g., 'sha256-59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf0')"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@else
|
||||
<x-forms.input id="docker_registry_image_name" label="Docker Image"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input id="docker_registry_image_tag" label="Docker Image Tag or Hash"
|
||||
<x-forms.input id="dockerRegistryImageName" label="Docker Image" x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input id="dockerRegistryImageTag" label="Docker Image Tag or Hash"
|
||||
helper="Enter a tag (e.g., 'latest', 'v1.2.3') or SHA256 hash (e.g., 'sha256-59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf0')"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
@else
|
||||
@if (
|
||||
$application->destination->server->isSwarm() ||
|
||||
$application->destination->server->isSwarm() ||
|
||||
$application->additional_servers->count() > 0 ||
|
||||
$application->settings->is_build_server_enabled)
|
||||
<x-forms.input id="docker_registry_image_name" required label="Docker Image"
|
||||
placeholder="Required!" x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input id="docker_registry_image_tag"
|
||||
$application->settings->is_build_server_enabled
|
||||
)
|
||||
<x-forms.input id="dockerRegistryImageName" required label="Docker Image" placeholder="Required!"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input id="dockerRegistryImageTag"
|
||||
helper="If set, it will tag the built image with this tag too. <br><br>Example: If you set it to 'latest', it will push the image with the commit sha tag + with the latest tag."
|
||||
placeholder="Empty means latest will be used." label="Docker Image Tag"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@else
|
||||
<x-forms.input id="docker_registry_image_name"
|
||||
<x-forms.input id="dockerRegistryImageName"
|
||||
helper="Empty means it won't push the image to a docker registry. Pre-tag the image with your registry url if you want to push it to a private registry (default: Dockerhub). <br><br>Example: ghcr.io/myimage"
|
||||
placeholder="Empty means it won't push the image to a docker registry."
|
||||
label="Docker Image" x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input id="docker_registry_image_tag"
|
||||
placeholder="Empty means only push commit sha tag."
|
||||
placeholder="Empty means it won't push the image to a docker registry." label="Docker Image"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input id="dockerRegistryImageTag" placeholder="Empty means only push commit sha tag."
|
||||
helper="If set, it will tag the built image with this tag too. <br><br>Example: If you set it to 'latest', it will push the image with the commit sha tag + with the latest tag."
|
||||
label="Docker Image Tag" x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
@@ -206,21 +214,17 @@
|
||||
<x-forms.input
|
||||
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
|
||||
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k --hostname=myapp"
|
||||
id="custom_docker_run_options" label="Custom Docker Options"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
id="customDockerRunOptions" label="Custom Docker Options" x-bind:disabled="!canUpdate" />
|
||||
@else
|
||||
@if ($application->could_set_build_commands())
|
||||
@if ($application->build_pack === 'nixpacks')
|
||||
<div class="flex flex-col gap-2 xl:flex-row">
|
||||
<x-forms.input helper="If you modify this, you probably need to have a nixpacks.toml"
|
||||
id="install_command" label="Install Command"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
id="installCommand" label="Install Command" x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input helper="If you modify this, you probably need to have a nixpacks.toml"
|
||||
id="build_command" label="Build Command"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
id="buildCommand" label="Build Command" x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input helper="If you modify this, you probably need to have a nixpacks.toml"
|
||||
id="start_command" label="Start Command"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
id="startCommand" label="Start Command" x-bind:disabled="!canUpdate" />
|
||||
</div>
|
||||
<div class="pt-1 text-xs">Nixpacks will detect the required configuration
|
||||
automatically.
|
||||
@@ -234,21 +238,18 @@
|
||||
@if ($application->build_pack === 'dockercompose')
|
||||
@can('update', $application)
|
||||
<div class="flex flex-col gap-2" x-init="$wire.dispatch('loadCompose', true)">
|
||||
@else
|
||||
@else
|
||||
<div class="flex flex-col gap-2">
|
||||
@endcan
|
||||
@endcan
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="/"
|
||||
id="base_directory" label="Base Directory"
|
||||
helper="Directory to use as root. Useful for monorepos." />
|
||||
<x-forms.input x-bind:disabled="shouldDisable()"
|
||||
placeholder="/docker-compose.yaml"
|
||||
id="docker_compose_location" label="Docker Compose Location"
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="/" id="baseDirectory"
|
||||
label="Base Directory" helper="Directory to use as root. Useful for monorepos." />
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="/docker-compose.yaml"
|
||||
id="dockerComposeLocation" label="Docker Compose Location"
|
||||
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }}</span>" />
|
||||
</div>
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox instantSave
|
||||
id="is_preserve_repository_enabled"
|
||||
<x-forms.checkbox instantSave id="isPreserveRepositoryEnabled"
|
||||
label="Preserve Repository During Deployment"
|
||||
helper="Git repository (based on the base directory settings) will be copied to the deployment directory."
|
||||
x-bind:disabled="shouldDisable()" />
|
||||
@@ -259,52 +260,59 @@
|
||||
know what are
|
||||
you doing.</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input x-bind:disabled="shouldDisable()"
|
||||
placeholder="docker compose build"
|
||||
id="docker_compose_custom_build_command"
|
||||
helper="If you use this, you need to specify paths relatively and should use the same compose file in the custom command, otherwise the automatically configured labels / etc won't work.<br><br>So in your case, use: <span class='dark:text-warning'>docker compose -f .{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }} build</span>"
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="docker compose build"
|
||||
id="dockerComposeCustomBuildCommand"
|
||||
helper="The compose file path (<span class='dark:text-warning'>-f</span> flag) and environment variables (<span class='dark:text-warning'>--env-file</span> flag) are automatically injected based on your Base Directory and Docker Compose Location settings. You can override by providing your own <span class='dark:text-warning'>-f</span> or <span class='dark:text-warning'>--env-file</span> flags.<br><br>If you use this, you need to specify paths relatively and should use the same compose file in the custom command, otherwise the automatically configured labels / etc won't work.<br><br>Example usage: <span class='dark:text-warning'>docker compose build</span>"
|
||||
label="Custom Build Command" />
|
||||
<x-forms.input x-bind:disabled="shouldDisable()"
|
||||
placeholder="docker compose up -d"
|
||||
id="docker_compose_custom_start_command"
|
||||
helper="If you use this, you need to specify paths relatively and should use the same compose file in the custom command, otherwise the automatically configured labels / etc won't work.<br><br>So in your case, use: <span class='dark:text-warning'>docker compose -f .{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }} up -d</span>"
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="docker compose up -d"
|
||||
id="dockerComposeCustomStartCommand"
|
||||
helper="The compose file path (<span class='dark:text-warning'>-f</span> flag) and environment variables (<span class='dark:text-warning'>--env-file</span> flag) are automatically injected based on your Base Directory and Docker Compose Location settings. You can override by providing your own <span class='dark:text-warning'>-f</span> or <span class='dark:text-warning'>--env-file</span> flags.<br><br>If you use this, you need to specify paths relatively and should use the same compose file in the custom command, otherwise the automatically configured labels / etc won't work.<br><br>Example usage: <span class='dark:text-warning'>docker compose up -d</span>"
|
||||
label="Custom Start Command" />
|
||||
</div>
|
||||
@if ($this->dockerComposeCustomBuildCommand)
|
||||
<div wire:key="docker-compose-build-preview">
|
||||
<x-forms.input readonly value="{{ $this->dockerComposeBuildCommandPreview }}"
|
||||
label="Final Build Command (Preview)"
|
||||
helper="This shows the actual command that will be executed with auto-injected flags." />
|
||||
</div>
|
||||
@endif
|
||||
@if ($this->dockerComposeCustomStartCommand)
|
||||
<div wire:key="docker-compose-start-preview">
|
||||
<x-forms.input readonly value="{{ $this->dockerComposeStartCommandPreview }}"
|
||||
label="Final Start Command (Preview)"
|
||||
helper="This shows the actual command that will be executed with auto-injected flags." />
|
||||
</div>
|
||||
@endif
|
||||
@if ($this->application->is_github_based() && !$this->application->is_public_repository())
|
||||
<div class="pt-4">
|
||||
<x-forms.textarea
|
||||
helper="Order-based pattern matching to filter Git webhook deployments. Supports wildcards (*, **, ?) and negation (!). Last matching pattern wins."
|
||||
placeholder="services/api/**" id="watch_paths"
|
||||
label="Watch Paths" x-bind:disabled="shouldDisable()" />
|
||||
placeholder="services/api/**" id="watchPaths" label="Watch Paths"
|
||||
x-bind:disabled="shouldDisable()" />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
@else
|
||||
<div class="flex flex-col gap-2 xl:flex-row">
|
||||
<x-forms.input placeholder="/" id="base_directory"
|
||||
label="Base Directory"
|
||||
helper="Directory to use as root. Useful for monorepos."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input placeholder="/" id="baseDirectory" label="Base Directory"
|
||||
helper="Directory to use as root. Useful for monorepos." x-bind:disabled="!canUpdate" />
|
||||
@if ($application->build_pack === 'dockerfile' && !$application->dockerfile)
|
||||
<x-forms.input placeholder="/Dockerfile" id="dockerfile_location"
|
||||
label="Dockerfile Location"
|
||||
<x-forms.input placeholder="/Dockerfile" id="dockerfileLocation" label="Dockerfile Location"
|
||||
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}</span>"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
|
||||
@if ($application->build_pack === 'dockerfile')
|
||||
<x-forms.input id="dockerfile_target_build"
|
||||
label="Docker Build Stage Target"
|
||||
helper="Useful if you have multi-staged dockerfile."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input id="dockerfileTargetBuild" label="Docker Build Stage Target"
|
||||
helper="Useful if you have multi-staged dockerfile." x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
@if ($application->could_set_build_commands())
|
||||
@if ($application->settings->is_static)
|
||||
<x-forms.input placeholder="/dist" id="publish_directory"
|
||||
label="Publish Directory" required x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input placeholder="/dist" id="publishDirectory" label="Publish Directory" required
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@else
|
||||
<x-forms.input placeholder="/" id="publish_directory"
|
||||
label="Publish Directory" x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input placeholder="/" id="publishDirectory" label="Publish Directory"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@@ -313,178 +321,221 @@
|
||||
<div class="pb-4">
|
||||
<x-forms.textarea
|
||||
helper="Order-based pattern matching to filter Git webhook deployments. Supports wildcards (*, **, ?) and negation (!). Last matching pattern wins."
|
||||
placeholder="src/pages/**" id="watch_paths"
|
||||
label="Watch Paths" x-bind:disabled="!canUpdate" />
|
||||
placeholder="src/pages/**" id="watchPaths" label="Watch Paths"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
</div>
|
||||
@endif
|
||||
<x-forms.input
|
||||
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
|
||||
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k --hostname=myapp"
|
||||
id="custom_docker_run_options" label="Custom Docker Options"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
id="customDockerRunOptions" label="Custom Docker Options" x-bind:disabled="!canUpdate" />
|
||||
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
<div class="pt-2 w-96">
|
||||
<x-forms.checkbox
|
||||
helper="Use a build server to build your application. You can configure your build server in the Server settings. For more info, check the <a href='https://coolify.io/docs/knowledge-base/server/build-server' class='underline' target='_blank'>documentation</a>."
|
||||
instantSave id="is_build_server_enabled"
|
||||
label="Use a Build Server?" x-bind:disabled="!canUpdate" />
|
||||
instantSave id="isBuildServerEnabled" label="Use a Build Server?"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@if ($application->build_pack === 'dockercompose')
|
||||
<div class="flex items-center gap-2 pb-4">
|
||||
<h3>Docker Compose</h3>
|
||||
@can('update', $application)
|
||||
<x-forms.button wire:target='initLoadingCompose'
|
||||
x-on:click="$wire.dispatch('loadCompose', false)">Reload Compose File</x-forms.button>
|
||||
@endcan
|
||||
</div>
|
||||
@if ($application->settings->is_raw_compose_deployment_enabled)
|
||||
<x-forms.textarea rows="10" readonly id="docker_compose_raw"
|
||||
label="Docker Compose Content (applicationId: {{ $application->id }})"
|
||||
helper="You need to modify the docker compose file in the git repository."
|
||||
monacoEditorLanguage="yaml" useMonacoEditor />
|
||||
@else
|
||||
@if ((int) $application->compose_parsing_version >= 3)
|
||||
<x-forms.textarea rows="10" readonly id="docker_compose_raw"
|
||||
label="Docker Compose Content (raw)"
|
||||
@if ($application->build_pack === 'dockercompose')
|
||||
<div x-data="{ showRaw: true }">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3>Docker Compose</h3>
|
||||
<x-forms.button x-show="!($application->settings->is_raw_compose_deployment_enabled)" @click.prevent="showRaw = !showRaw" x-text="showRaw ? 'Show Deployable Compose' : 'Show Raw Compose'"></x-forms.button>
|
||||
</div>
|
||||
@if ($application->settings->is_raw_compose_deployment_enabled)
|
||||
<x-forms.textarea rows="10" readonly id="dockerComposeRaw"
|
||||
label="Docker Compose Content (applicationId: {{ $application->id }})"
|
||||
helper="You need to modify the docker compose file in the git repository."
|
||||
monacoEditorLanguage="yaml" useMonacoEditor />
|
||||
@endif
|
||||
<x-forms.textarea rows="10" readonly id="docker_compose"
|
||||
label="Docker Compose Content"
|
||||
helper="You need to modify the docker compose file in the git repository."
|
||||
monacoEditorLanguage="yaml" useMonacoEditor />
|
||||
@endif
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox label="Escape special characters in labels?"
|
||||
helper="By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$.<br><br>If you want to use env variables inside the labels, turn this off."
|
||||
id="is_container_label_escape_enabled" instantSave
|
||||
x-bind:disabled="!canUpdate"></x-forms.checkbox>
|
||||
{{-- <x-forms.checkbox label="Readonly labels"
|
||||
helper="Labels are readonly by default. Readonly means that edits you do to the labels could be lost and Coolify will autogenerate the labels for you. If you want to edit the labels directly, disable this option. <br><br>Be careful, it could break the proxy configuration after you restart the container as Coolify will now NOT autogenerate the labels for you (ofc you can always reset the labels to the coolify defaults manually)."
|
||||
id="is_container_label_readonly_enabled" instantSave></x-forms.checkbox> --}}
|
||||
</div>
|
||||
@endif
|
||||
@if ($application->dockerfile)
|
||||
<x-forms.textarea label="Dockerfile" id="dockerfile" monacoEditorLanguage="dockerfile"
|
||||
useMonacoEditor rows="6" x-bind:disabled="!canUpdate"> </x-forms.textarea>
|
||||
@endif
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
<h3 class="pt-8">Network</h3>
|
||||
<div class="flex flex-col gap-2 xl:flex-row">
|
||||
@if ($application->settings->is_static || $application->build_pack === 'static')
|
||||
<x-forms.input id="ports_exposes" label="Ports Exposes" readonly
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@else
|
||||
@if ($application->settings->is_container_label_readonly_enabled === false)
|
||||
<x-forms.input placeholder="3000,3001" id="ports_exposes"
|
||||
label="Ports Exposes" readonly
|
||||
helper="Readonly labels are disabled. You can set the ports manually in the labels section."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@else
|
||||
<x-forms.input placeholder="3000,3001" id="ports_exposes"
|
||||
label="Ports Exposes" required
|
||||
helper="A comma separated list of ports your application uses. The first port will be used as default healthcheck port if nothing defined in the Healthcheck menu. Be sure to set this correctly."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@if ((int) $application->compose_parsing_version >= 3)
|
||||
<div x-show="showRaw">
|
||||
<x-forms.textarea rows="10" readonly id="dockerComposeRaw" label="Docker Compose Content (raw)"
|
||||
helper="You need to modify the docker compose file in the git repository."
|
||||
monacoEditorLanguage="yaml" useMonacoEditor />
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<x-forms.input placeholder="3000:3000" id="ports_mappings" label="Ports Mappings"
|
||||
helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold dark:text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<x-forms.input id="custom_network_aliases" label="Network Aliases"
|
||||
helper="A comma separated list of custom network aliases you would like to add for container in Docker network.<br><br><span class='inline-block font-bold dark:text-warning'>Example:</span><br>api.internal,api.local"
|
||||
wire:model="custom_network_aliases" x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<h3 class="pt-8">HTTP Basic Authentication</h3>
|
||||
<div>
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox helper="This will add the proper proxy labels to the container." instantSave
|
||||
label="Enable" id="is_http_basic_auth_enabled"
|
||||
x-bind:disabled="!canUpdate" />
|
||||
</div>
|
||||
@if ($application->is_http_basic_auth_enabled)
|
||||
<div class="flex gap-2 py-2">
|
||||
<x-forms.input id="http_basic_auth_username" label="Username" required
|
||||
x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input id="http_basic_auth_password" type="password" label="Password"
|
||||
required x-bind:disabled="!canUpdate" />
|
||||
<div x-show="showRaw === false">
|
||||
<x-forms.textarea rows="10" readonly id="dockerCompose" label="Docker Compose Content"
|
||||
helper="You need to modify the docker compose file in the git repository."
|
||||
monacoEditorLanguage="yaml" useMonacoEditor />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($application->settings->is_container_label_readonly_enabled)
|
||||
<x-forms.textarea readonly disabled label="Container Labels" rows="15" id="customLabels"
|
||||
monacoEditorLanguage="ini" useMonacoEditor x-bind:disabled="!canUpdate"></x-forms.textarea>
|
||||
@else
|
||||
<x-forms.textarea label="Container Labels" rows="15" id="customLabels"
|
||||
monacoEditorLanguage="ini" useMonacoEditor x-bind:disabled="!canUpdate"></x-forms.textarea>
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox label="Escape special characters in labels?"
|
||||
helper="By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$.<br><br>If you want to use env variables inside the labels, turn this off."
|
||||
id="isContainerLabelEscapeEnabled" instantSave x-bind:disabled="!canUpdate"></x-forms.checkbox>
|
||||
{{-- <x-forms.checkbox label="Readonly labels"
|
||||
helper="Labels are readonly by default. Readonly means that edits you do to the labels could be lost and Coolify will autogenerate the labels for you. If you want to edit the labels directly, disable this option. <br><br>Be careful, it could break the proxy configuration after you restart the container as Coolify will now NOT autogenerate the labels for you (ofc you can always reset the labels to the coolify defaults manually)."
|
||||
id="isContainerLabelReadonlyEnabled" instantSave></x-forms.checkbox> --}}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox label="Readonly labels"
|
||||
helper="Labels are readonly by default. Readonly means that edits you do to the labels could be lost and Coolify will autogenerate the labels for you. If you want to edit the labels directly, disable this option. <br><br>Be careful, it could break the proxy configuration after you restart the container as Coolify will now NOT autogenerate the labels for you (ofc you can always reset the labels to the coolify defaults manually)."
|
||||
id="is_container_label_readonly_enabled" instantSave
|
||||
x-bind:disabled="!canUpdate"></x-forms.checkbox>
|
||||
<x-forms.checkbox label="Escape special characters in labels?"
|
||||
helper="By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$.<br><br>If you want to use env variables inside the labels, turn this off."
|
||||
id="is_container_label_escape_enabled" instantSave
|
||||
x-bind:disabled="!canUpdate"></x-forms.checkbox>
|
||||
</div>
|
||||
@can('update', $application)
|
||||
<x-modal-confirmation title="Confirm Labels Reset to Coolify Defaults?"
|
||||
buttonTitle="Reset Labels to Defaults" buttonFullWidth submitAction="resetDefaultLabels(true)"
|
||||
:actions="[
|
||||
@if ($application->dockerfile)
|
||||
<x-forms.textarea label="Dockerfile" id="dockerfile" monacoEditorLanguage="dockerfile" useMonacoEditor
|
||||
rows="6" x-bind:disabled="!canUpdate"> </x-forms.textarea>
|
||||
@endif
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
<h3 class="pt-8">Network</h3>
|
||||
@if ($this->detectedPortInfo)
|
||||
@if ($this->detectedPortInfo['isEmpty'])
|
||||
<div
|
||||
class="flex items-start gap-2 p-4 mb-4 text-sm rounded-lg bg-yellow-50 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300 border border-yellow-200 dark:border-yellow-800">
|
||||
<svg class="w-5 h-5 shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<div>
|
||||
<span class="font-semibold">PORT environment variable detected
|
||||
({{ $this->detectedPortInfo['port'] }})</span>
|
||||
<p class="mt-1">Your Ports Exposes field is empty. Consider setting it to
|
||||
<strong>{{ $this->detectedPortInfo['port'] }}</strong> to ensure the proxy routes traffic
|
||||
correctly.</p>
|
||||
</div>
|
||||
</div>
|
||||
@elseif (!$this->detectedPortInfo['matches'])
|
||||
<div
|
||||
class="flex items-start gap-2 p-4 mb-4 text-sm rounded-lg bg-yellow-50 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300 border border-yellow-200 dark:border-yellow-800">
|
||||
<svg class="w-5 h-5 shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<div>
|
||||
<span class="font-semibold">PORT mismatch detected</span>
|
||||
<p class="mt-1">Your PORT environment variable is set to
|
||||
<strong>{{ $this->detectedPortInfo['port'] }}</strong>, but it's not in your Ports Exposes
|
||||
configuration. Ensure they match for proper proxy routing.</p>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div
|
||||
class="flex items-start gap-2 p-4 mb-4 text-sm rounded-lg bg-blue-50 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300 border border-blue-200 dark:border-blue-800">
|
||||
<svg class="w-5 h-5 shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<div>
|
||||
<span class="font-semibold">PORT environment variable configured</span>
|
||||
<p class="mt-1">Your PORT environment variable ({{ $this->detectedPortInfo['port'] }}) matches
|
||||
your Ports Exposes configuration.</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
<div class="flex flex-col gap-2 xl:flex-row">
|
||||
@if ($application->settings->is_static || $application->build_pack === 'static')
|
||||
<x-forms.input id="portsExposes" label="Ports Exposes" readonly x-bind:disabled="!canUpdate" />
|
||||
@else
|
||||
@if ($application->settings->is_container_label_readonly_enabled === false)
|
||||
<x-forms.input placeholder="3000,3001" id="portsExposes" label="Ports Exposes" readonly
|
||||
helper="Readonly labels are disabled. You can set the ports manually in the labels section."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@else
|
||||
<x-forms.input placeholder="3000,3001" id="portsExposes" label="Ports Exposes" required
|
||||
helper="A comma separated list of ports your application uses. The first port will be used as default healthcheck port if nothing defined in the Healthcheck menu. Be sure to set this correctly."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
@endif
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<x-forms.input placeholder="3000:3000" id="portsMappings" label="Ports Mappings"
|
||||
helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold dark:text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<x-forms.input id="customNetworkAliases" label="Network Aliases"
|
||||
helper="A comma separated list of custom network aliases you would like to add for container in Docker network.<br><br><span class='inline-block font-bold dark:text-warning'>Example:</span><br>api.internal,api.local"
|
||||
wire:model="customNetworkAliases" x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<h3 class="pt-8">HTTP Basic Authentication</h3>
|
||||
<div>
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox helper="This will add the proper proxy labels to the container." instantSave
|
||||
label="Enable" id="isHttpBasicAuthEnabled" x-bind:disabled="!canUpdate" />
|
||||
</div>
|
||||
@if ($application->is_http_basic_auth_enabled)
|
||||
<div class="flex gap-2 py-2">
|
||||
<x-forms.input id="httpBasicAuthUsername" label="Username" required
|
||||
x-bind:disabled="!canUpdate" />
|
||||
<x-forms.input id="httpBasicAuthPassword" type="password" label="Password" required
|
||||
x-bind:disabled="!canUpdate" />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($application->settings->is_container_label_readonly_enabled)
|
||||
<x-forms.textarea readonly disabled label="Container Labels" rows="15" id="customLabels"
|
||||
monacoEditorLanguage="ini" useMonacoEditor x-bind:disabled="!canUpdate"></x-forms.textarea>
|
||||
@else
|
||||
<x-forms.textarea label="Container Labels" rows="15" id="customLabels" monacoEditorLanguage="ini"
|
||||
useMonacoEditor x-bind:disabled="!canUpdate"></x-forms.textarea>
|
||||
@endif
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox label="Readonly labels"
|
||||
helper="Labels are readonly by default. Readonly means that edits you do to the labels could be lost and Coolify will autogenerate the labels for you. If you want to edit the labels directly, disable this option. <br><br>Be careful, it could break the proxy configuration after you restart the container as Coolify will now NOT autogenerate the labels for you (ofc you can always reset the labels to the coolify defaults manually)."
|
||||
id="isContainerLabelReadonlyEnabled" instantSave
|
||||
x-bind:disabled="!canUpdate"></x-forms.checkbox>
|
||||
<x-forms.checkbox label="Escape special characters in labels?"
|
||||
helper="By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$.<br><br>If you want to use env variables inside the labels, turn this off."
|
||||
id="isContainerLabelEscapeEnabled" instantSave x-bind:disabled="!canUpdate"></x-forms.checkbox>
|
||||
</div>
|
||||
@can('update', $application)
|
||||
<x-modal-confirmation title="Confirm Labels Reset to Coolify Defaults?"
|
||||
buttonTitle="Reset Labels to Defaults" buttonFullWidth submitAction="resetDefaultLabels(true)"
|
||||
:actions="[
|
||||
'All your custom proxy labels will be lost.',
|
||||
'Proxy labels (traefik, caddy, etc) will be reset to the coolify defaults.',
|
||||
]" confirmationText="{{ $application->fqdn . '/' }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Application URL below"
|
||||
shortConfirmationLabel="Application URL" :confirmWithPassword="false"
|
||||
step2ButtonText="Permanently Reset Labels" />
|
||||
@endcan
|
||||
@endif
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Application URL below"
|
||||
shortConfirmationLabel="Application URL" :confirmWithPassword="false"
|
||||
step2ButtonText="Permanently Reset Labels" />
|
||||
@endcan
|
||||
@endif
|
||||
|
||||
<h3 class="pt-8">Pre/Post Deployment Commands</h3>
|
||||
<div class="flex flex-col gap-2 xl:flex-row">
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="php artisan migrate"
|
||||
id="pre_deployment_command" label="Pre-deployment "
|
||||
helper="An optional script or command to execute in the existing container before the deployment begins.<br>It is always executed with 'sh -c', so you do not need add it manually." />
|
||||
@if ($application->build_pack === 'dockercompose')
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" id="pre_deployment_command_container"
|
||||
label="Container Name"
|
||||
helper="The name of the container to execute within. You can leave it blank if your application only has one container." />
|
||||
@endif
|
||||
<h3 class="pt-8">Pre/Post Deployment Commands</h3>
|
||||
<div class="flex flex-col gap-2 xl:flex-row">
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="php artisan migrate"
|
||||
id="preDeploymentCommand" label="Pre-deployment "
|
||||
helper="An optional script or command to execute in the existing container before the deployment begins.<br>It is always executed with 'sh -c', so you do not need add it manually." />
|
||||
@if ($application->build_pack === 'dockercompose')
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" id="preDeploymentCommandContainer"
|
||||
label="Container Name"
|
||||
helper="The name of the container to execute within. You can leave it blank if your application only has one container." />
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 xl:flex-row">
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="php artisan migrate"
|
||||
id="postDeploymentCommand" label="Post-deployment "
|
||||
helper="An optional script or command to execute in the newly built container after the deployment completes.<br>It is always executed with 'sh -c', so you do not need add it manually." />
|
||||
@if ($application->build_pack === 'dockercompose')
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" id="postDeploymentCommandContainer"
|
||||
label="Container Name"
|
||||
helper="The name of the container to execute within. You can leave it blank if your application only has one container." />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 xl:flex-row">
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="php artisan migrate"
|
||||
id="post_deployment_command" label="Post-deployment "
|
||||
helper="An optional script or command to execute in the newly built container after the deployment completes.<br>It is always executed with 'sh -c', so you do not need add it manually." />
|
||||
@if ($application->build_pack === 'dockercompose')
|
||||
<x-forms.input x-bind:disabled="shouldDisable()"
|
||||
id="post_deployment_command_container" label="Container Name"
|
||||
helper="The name of the container to execute within. You can leave it blank if your application only has one container." />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<x-domain-conflict-modal :conflicts="$domainConflicts" :showModal="$showDomainConflictModal" confirmAction="confirmDomainUsage" />
|
||||
<x-domain-conflict-modal :conflicts="$domainConflicts" :showModal="$showDomainConflictModal"
|
||||
confirmAction="confirmDomainUsage" />
|
||||
|
||||
@script
|
||||
<script>
|
||||
$wire.$on('loadCompose', (isInit = true) => {
|
||||
// Only load compose file if user has permission (this event should only be dispatched when authorized)
|
||||
$wire.initLoadingCompose = true;
|
||||
$wire.loadComposeFile(isInit);
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
$wire.$on('loadCompose', (isInit = true) => {
|
||||
// Only load compose file if user has permission (this event should only be dispatched when authorized)
|
||||
$wire.initLoadingCompose = true;
|
||||
$wire.loadComposeFile(isInit);
|
||||
});
|
||||
</script>
|
||||
@endscript
|
||||
</div>
|
||||
</div>
|
||||
@@ -12,7 +12,14 @@
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('project.application.logs') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('project.application.logs', $parameters) }}">
|
||||
Logs
|
||||
<div class="flex items-center gap-1">
|
||||
Logs
|
||||
@if ($application->restart_count > 0 && !str($application->status)->startsWith('exited'))
|
||||
<svg class="w-4 h-4 dark:text-warning" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" title="Container has restarted {{ $application->restart_count }} time{{ $application->restart_count > 1 ? 's' : '' }}">
|
||||
<path d="M12 2L1 21h22L12 2zm0 4l7.53 13H4.47L12 6zm-1 5v4h2v-4h-2zm0 5v2h2v-2h-2z"/>
|
||||
</svg>
|
||||
@endif
|
||||
</div>
|
||||
</a>
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
@can('canAccessTerminal')
|
||||
|
||||
@@ -68,22 +68,21 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-gray-600 dark:text-gray-400 text-sm">
|
||||
Started: {{ formatDateInServerTimezone(data_get($execution, 'created_at'), $this->server()) }}
|
||||
@if (data_get($execution, 'status') !== 'running')
|
||||
<br>Ended:
|
||||
{{ formatDateInServerTimezone(data_get($execution, 'finished_at'), $this->server()) }}
|
||||
<br>Duration:
|
||||
{{ calculateDuration(data_get($execution, 'created_at'), data_get($execution, 'finished_at')) }}
|
||||
<br>Finished {{ \Carbon\Carbon::parse(data_get($execution, 'finished_at'))->diffForHumans() }}
|
||||
@if (data_get($execution, 'status') === 'running')
|
||||
<span title="Started: {{ formatDateInServerTimezone(data_get($execution, 'created_at'), $this->server()) }}">
|
||||
Running for {{ calculateDuration(data_get($execution, 'created_at'), now()) }}
|
||||
</span>
|
||||
@else
|
||||
<span title="Started: {{ formatDateInServerTimezone(data_get($execution, 'created_at'), $this->server()) }} Ended: {{ formatDateInServerTimezone(data_get($execution, 'finished_at'), $this->server()) }}">
|
||||
{{ \Carbon\Carbon::parse(data_get($execution, 'finished_at'))->diffForHumans() }}
|
||||
({{ calculateDuration(data_get($execution, 'created_at'), data_get($execution, 'finished_at')) }})
|
||||
• {{ \Carbon\Carbon::parse(data_get($execution, 'finished_at'))->format('M j, H:i') }}
|
||||
</span>
|
||||
@endif
|
||||
• Database: {{ data_get($execution, 'database_name', 'N/A') }}
|
||||
@if(data_get($execution, 'size'))
|
||||
• Size: {{ formatBytes(data_get($execution, 'size')) }}
|
||||
@endif
|
||||
</div>
|
||||
<div class="text-gray-600 dark:text-gray-400 text-sm">
|
||||
Database: {{ data_get($execution, 'database_name', 'N/A') }}
|
||||
</div>
|
||||
<div class="text-gray-600 dark:text-gray-400 text-sm">
|
||||
Size: {{ data_get($execution, 'size') }} B /
|
||||
{{ round((int) data_get($execution, 'size') / 1024, 2) }} kB /
|
||||
{{ round((int) data_get($execution, 'size') / 1024 / 1024, 3) }} MB
|
||||
</div>
|
||||
<div class="text-gray-600 dark:text-gray-400 text-sm">
|
||||
Location: {{ data_get($execution, 'filename', 'N/A') }}
|
||||
|
||||
@@ -1,54 +1,65 @@
|
||||
<div x-data="{ error: $wire.entangle('error'), filesize: $wire.entangle('filesize'), filename: $wire.entangle('filename'), isUploading: $wire.entangle('isUploading'), progress: $wire.entangle('progress') }">
|
||||
<div x-data="{
|
||||
error: $wire.entangle('error'),
|
||||
filesize: $wire.entangle('filesize'),
|
||||
filename: $wire.entangle('filename'),
|
||||
isUploading: $wire.entangle('isUploading'),
|
||||
progress: $wire.entangle('progress'),
|
||||
s3FileSize: $wire.entangle('s3FileSize'),
|
||||
s3StorageId: $wire.entangle('s3StorageId'),
|
||||
s3Path: $wire.entangle('s3Path'),
|
||||
restoreType: null
|
||||
}">
|
||||
<script type="text/javascript" src="{{ URL::asset('js/dropzone.js') }}"></script>
|
||||
@script
|
||||
<script data-navigate-once>
|
||||
Dropzone.options.myDropzone = {
|
||||
chunking: true,
|
||||
method: "POST",
|
||||
maxFilesize: 1000000000,
|
||||
chunkSize: 10000000,
|
||||
createImageThumbnails: false,
|
||||
disablePreviews: true,
|
||||
parallelChunkUploads: false,
|
||||
init: function() {
|
||||
let button = this.element.querySelector('button');
|
||||
button.innerText = 'Select or drop a backup file here.'
|
||||
this.on('sending', function(file, xhr, formData) {
|
||||
const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
formData.append("_token", token);
|
||||
});
|
||||
this.on("addedfile", file => {
|
||||
$wire.isUploading = true;
|
||||
});
|
||||
this.on('uploadprogress', function(file, progress, bytesSent) {
|
||||
$wire.progress = progress;
|
||||
});
|
||||
this.on('complete', function(file) {
|
||||
$wire.filename = file.name;
|
||||
$wire.filesize = Number(file.size / 1024 / 1024).toFixed(2) + ' MB';
|
||||
$wire.isUploading = false;
|
||||
});
|
||||
this.on('error', function(file, message) {
|
||||
$wire.error = true;
|
||||
$wire.$dispatch('error', message.error)
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<script data-navigate-once>
|
||||
Dropzone.options.myDropzone = {
|
||||
chunking: true,
|
||||
method: "POST",
|
||||
maxFilesize: 1000000000,
|
||||
chunkSize: 10000000,
|
||||
createImageThumbnails: false,
|
||||
disablePreviews: true,
|
||||
parallelChunkUploads: false,
|
||||
init: function () {
|
||||
let button = this.element.querySelector('button');
|
||||
button.innerText = 'Select or drop a backup file here.'
|
||||
this.on('sending', function (file, xhr, formData) {
|
||||
const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
formData.append("_token", token);
|
||||
});
|
||||
this.on("addedfile", file => {
|
||||
$wire.isUploading = true;
|
||||
$wire.customLocation = '';
|
||||
});
|
||||
this.on('uploadprogress', function (file, progress, bytesSent) {
|
||||
$wire.progress = progress;
|
||||
});
|
||||
this.on('complete', function (file) {
|
||||
$wire.filename = file.name;
|
||||
$wire.filesize = Number(file.size / 1024 / 1024).toFixed(2) + ' MB';
|
||||
$wire.isUploading = false;
|
||||
});
|
||||
this.on('error', function (file, message) {
|
||||
$wire.error = true;
|
||||
$wire.$dispatch('error', message.error)
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@endscript
|
||||
<h2>Import Backup</h2>
|
||||
@if ($unsupported)
|
||||
<div>Database restore is not supported.</div>
|
||||
@else
|
||||
<div class="pt-2 rounded-sm alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span>This is a destructive action, existing data will be replaced!</span>
|
||||
</div>
|
||||
@if (str(data_get($resource, 'status'))->startsWith('running'))
|
||||
{{-- Restore Command Configuration --}}
|
||||
@if ($resource->type() === 'standalone-postgresql')
|
||||
@if ($dumpAll)
|
||||
<x-forms.textarea rows="6" readonly label="Custom Import Command"
|
||||
@@ -62,8 +73,7 @@
|
||||
</div>
|
||||
@endif
|
||||
<div class="w-64 pt-2">
|
||||
<x-forms.checkbox label="Backup includes all databases"
|
||||
wire:model.live='dumpAll'></x-forms.checkbox>
|
||||
<x-forms.checkbox label="Backup includes all databases" wire:model.live='dumpAll'></x-forms.checkbox>
|
||||
</div>
|
||||
@elseif ($resource->type() === 'standalone-mysql')
|
||||
@if ($dumpAll)
|
||||
@@ -73,8 +83,7 @@
|
||||
<x-forms.input label="Custom Import Command" wire:model='mysqlRestoreCommand'></x-forms.input>
|
||||
@endif
|
||||
<div class="w-64 pt-2">
|
||||
<x-forms.checkbox label="Backup includes all databases"
|
||||
wire:model.live='dumpAll'></x-forms.checkbox>
|
||||
<x-forms.checkbox label="Backup includes all databases" wire:model.live='dumpAll'></x-forms.checkbox>
|
||||
</div>
|
||||
@elseif ($resource->type() === 'standalone-mariadb')
|
||||
@if ($dumpAll)
|
||||
@@ -84,35 +93,143 @@
|
||||
<x-forms.input label="Custom Import Command" wire:model='mariadbRestoreCommand'></x-forms.input>
|
||||
@endif
|
||||
<div class="w-64 pt-2">
|
||||
<x-forms.checkbox label="Backup includes all databases"
|
||||
wire:model.live='dumpAll'></x-forms.checkbox>
|
||||
<x-forms.checkbox label="Backup includes all databases" wire:model.live='dumpAll'></x-forms.checkbox>
|
||||
</div>
|
||||
@endif
|
||||
<h3 class="pt-6">Backup File</h3>
|
||||
<form class="flex gap-2 items-end">
|
||||
<x-forms.input label="Location of the backup file on the server"
|
||||
placeholder="e.g. /home/user/backup.sql.gz" wire:model='customLocation'></x-forms.input>
|
||||
<x-forms.button class="w-full" wire:click='checkFile'>Check File</x-forms.button>
|
||||
</form>
|
||||
<div class="pt-2 text-center text-xl font-bold">
|
||||
Or
|
||||
</div>
|
||||
<form action="/upload/backup/{{ $resource->uuid }}" class="dropzone" id="my-dropzone" wire:ignore>
|
||||
@csrf
|
||||
</form>
|
||||
<div x-show="isUploading">
|
||||
<progress max="100" x-bind:value="progress" class="progress progress-warning"></progress>
|
||||
</div>
|
||||
<h3 class="pt-6" x-show="filename && !error">File Information</h3>
|
||||
<div x-show="filename && !error">
|
||||
<div>Location: <span x-text="filename ?? 'N/A'"></span> <span x-text="filesize">/ </span></div>
|
||||
<x-forms.button class="w-full my-4" wire:click='runImport'>Restore Backup</x-forms.button>
|
||||
</div>
|
||||
<div class="container w-full mx-auto" x-show="$wire.importRunning">
|
||||
<livewire:activity-monitor header="Database Restore Output" :showWaiting="false" />
|
||||
|
||||
{{-- Restore Type Selection Boxes --}}
|
||||
<h3 class="pt-6">Choose Restore Method</h3>
|
||||
<div class="flex gap-4 pt-2">
|
||||
<div @click="restoreType = 'file'"
|
||||
class="flex-1 p-6 border-2 rounded-sm cursor-pointer transition-all"
|
||||
:class="restoreType === 'file' ? 'border-warning bg-warning/10' : 'border-neutral-200 dark:border-neutral-800 hover:border-warning/50'">
|
||||
<div class="flex flex-col gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||
</svg>
|
||||
<h4 class="text-lg font-bold">Restore from File</h4>
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400">Upload a backup file or specify a file path on the server</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($availableS3Storages->count() > 0)
|
||||
<div @click="restoreType = 's3'"
|
||||
class="flex-1 p-6 border-2 rounded-sm cursor-pointer transition-all"
|
||||
:class="restoreType === 's3' ? 'border-warning bg-warning/10' : 'border-neutral-200 dark:border-neutral-800 hover:border-warning/50'">
|
||||
<div class="flex flex-col gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
|
||||
</svg>
|
||||
<h4 class="text-lg font-bold">Restore from S3</h4>
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400">Download and restore a backup from S3 storage</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- File Restore Section --}}
|
||||
@can('update', $resource)
|
||||
<div x-show="restoreType === 'file'" class="pt-6">
|
||||
<h3>Backup File</h3>
|
||||
<form class="flex gap-2 items-end pt-2">
|
||||
<x-forms.input label="Location of the backup file on the server" placeholder="e.g. /home/user/backup.sql.gz"
|
||||
wire:model='customLocation' x-model="$wire.customLocation"></x-forms.input>
|
||||
<x-forms.button class="w-full" wire:click='checkFile' x-bind:disabled="!$wire.customLocation">Check File</x-forms.button>
|
||||
</form>
|
||||
<div class="pt-2 text-center text-xl font-bold">
|
||||
Or
|
||||
</div>
|
||||
<form action="/upload/backup/{{ $resource->uuid }}" class="dropzone" id="my-dropzone" wire:ignore>
|
||||
@csrf
|
||||
</form>
|
||||
<div x-show="isUploading">
|
||||
<progress max="100" x-bind:value="progress" class="progress progress-warning"></progress>
|
||||
</div>
|
||||
|
||||
<div x-show="filename && !error" class="pt-6">
|
||||
<h3>File Information</h3>
|
||||
<div class="pt-2">Location: <span x-text="filename ?? 'N/A'"></span><span x-show="filesize" x-text="' / ' + filesize"></span></div>
|
||||
<div class="pt-2">
|
||||
<x-modal-confirmation title="Restore Database from File?" buttonTitle="Restore from File"
|
||||
submitAction="runImport" isErrorButton>
|
||||
<x-slot:button-title>
|
||||
Restore Database from File
|
||||
</x-slot:button-title>
|
||||
This will perform the following actions:
|
||||
<ul class="list-disc list-inside pt-2">
|
||||
<li>Copy backup file to database container</li>
|
||||
<li>Execute restore command</li>
|
||||
</ul>
|
||||
<div class="pt-2 font-bold text-error">WARNING: This will REPLACE all existing data!</div>
|
||||
</x-modal-confirmation>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
|
||||
{{-- S3 Restore Section --}}
|
||||
@if ($availableS3Storages->count() > 0)
|
||||
@can('update', $resource)
|
||||
<div x-show="restoreType === 's3'" class="pt-6">
|
||||
<h3>Restore from S3</h3>
|
||||
<div class="flex flex-col gap-2 pt-2">
|
||||
<x-forms.select label="S3 Storage" wire:model.live="s3StorageId">
|
||||
<option value="">Select S3 Storage</option>
|
||||
@foreach ($availableS3Storages as $storage)
|
||||
<option value="{{ $storage->id }}">{{ $storage->name }}
|
||||
@if ($storage->description)
|
||||
- {{ $storage->description }}
|
||||
@endif
|
||||
</option>
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
|
||||
<x-forms.input label="S3 File Path (within bucket)"
|
||||
helper="Path to the backup file in your S3 bucket, e.g., /backups/database-2025-01-15.gz"
|
||||
placeholder="/backups/database-backup.gz" wire:model.blur='s3Path'
|
||||
wire:keydown.enter='checkS3File'></x-forms.input>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<x-forms.button class="w-full" wire:click='checkS3File' x-bind:disabled="!s3StorageId || !s3Path">
|
||||
Check File
|
||||
</x-forms.button>
|
||||
</div>
|
||||
|
||||
@if ($s3FileSize)
|
||||
<div class="pt-6">
|
||||
<h3>File Information</h3>
|
||||
<div class="pt-2">Location: {{ $s3Path }} {{ formatBytes($s3FileSize ?? 0) }}</div>
|
||||
<div class="pt-2">
|
||||
<x-modal-confirmation title="Restore Database from S3?" buttonTitle="Restore from S3"
|
||||
submitAction="restoreFromS3" isErrorButton>
|
||||
<x-slot:button-title>
|
||||
Restore Database from S3
|
||||
</x-slot:button-title>
|
||||
This will perform the following actions:
|
||||
<ul class="list-disc list-inside pt-2">
|
||||
<li>Download backup from S3 storage</li>
|
||||
<li>Copy file into database container</li>
|
||||
<li>Execute restore command</li>
|
||||
</ul>
|
||||
<div class="pt-2 font-bold text-error">WARNING: This will REPLACE all existing data!</div>
|
||||
</x-modal-confirmation>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
@endif
|
||||
|
||||
{{-- Slide-over for activity monitor (all restore operations) --}}
|
||||
<x-slide-over @databaserestore.window="slideOverOpen = true" closeWithX fullScreen>
|
||||
<x-slot:title>Database Restore Output</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:activity-monitor wire:key="database-restore-{{ $resource->uuid }}" header="Logs" fullHeight />
|
||||
</x-slot:content>
|
||||
</x-slide-over>
|
||||
@else
|
||||
<div>Database must be running to restore a backup.</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -31,11 +31,11 @@
|
||||
</div>
|
||||
@if ($database->started_at)
|
||||
<div class="flex xl:flex-row flex-col gap-2">
|
||||
<x-forms.input label="Username" id="postgresUser" placeholder="If empty: postgres"
|
||||
canGate="update" :canResource="$database"
|
||||
<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"
|
||||
<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." />
|
||||
<x-forms.input label="Initial Database" id="postgresDb"
|
||||
placeholder="If empty, it will be the same as Username." readonly
|
||||
@@ -43,19 +43,19 @@
|
||||
</div>
|
||||
@else
|
||||
<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" />
|
||||
<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" />
|
||||
<x-forms.input label="Initial Database" id="postgresDb"
|
||||
placeholder="If empty, it will be the same as Username." canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input label="Initial Database Arguments" canGate="update" :canResource="$database"
|
||||
id="postgresInitdbArgs" placeholder="If empty, use default. See in docker docs." />
|
||||
<x-forms.input label="Host Auth Method" canGate="update" :canResource="$database"
|
||||
id="postgresHostAuthMethod" placeholder="If empty, use default. See in docker docs." />
|
||||
<x-forms.input label="Initial Database Arguments" canGate="update" :canResource="$database" id="postgresInitdbArgs"
|
||||
placeholder="If empty, use default. See in docker docs." />
|
||||
<x-forms.input label="Host Auth Method" canGate="update" :canResource="$database" id="postgresHostAuthMethod"
|
||||
placeholder="If empty, use default. See in docker docs." />
|
||||
</div>
|
||||
<x-forms.input
|
||||
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
|
||||
@@ -107,20 +107,19 @@
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="w-64" wire:key='enable_ssl'>
|
||||
@if ($database->isExited())
|
||||
<x-forms.checkbox id="enableSsl" label="Enable SSL"
|
||||
wire:model.live="enableSsl" instantSave="instantSaveSSL" canGate="update"
|
||||
:canResource="$database" />
|
||||
<x-forms.checkbox id="enableSsl" label="Enable SSL" wire:model.live="enableSsl"
|
||||
instantSave="instantSaveSSL" canGate="update" :canResource="$database" />
|
||||
@else
|
||||
<x-forms.checkbox id="enableSsl" label="Enable SSL"
|
||||
wire:model.live="enableSsl" instantSave="instantSaveSSL" disabled
|
||||
<x-forms.checkbox id="enableSsl" label="Enable SSL" wire:model.live="enableSsl"
|
||||
instantSave="instantSaveSSL" disabled
|
||||
helper="Database should be stopped to change this settings." />
|
||||
@endif
|
||||
</div>
|
||||
@if ($enableSsl)
|
||||
<div class="mx-2">
|
||||
@if ($database->isExited())
|
||||
<x-forms.select id="sslMode" label="SSL Mode"
|
||||
wire:model.live="sslMode" instantSave="instantSaveSSL"
|
||||
<x-forms.select id="sslMode" label="SSL Mode" wire:model.live="sslMode"
|
||||
instantSave="instantSaveSSL"
|
||||
helper="Choose the SSL verification mode for PostgreSQL connections" canGate="update"
|
||||
:canResource="$database">
|
||||
<option value="allow" title="Allow insecure connections">allow (insecure)</option>
|
||||
@@ -131,8 +130,8 @@
|
||||
</option>
|
||||
</x-forms.select>
|
||||
@else
|
||||
<x-forms.select id="sslMode" label="SSL Mode" instantSave="instantSaveSSL"
|
||||
disabled helper="Database should be stopped to change this settings.">
|
||||
<x-forms.select id="sslMode" label="SSL Mode" instantSave="instantSaveSSL" disabled
|
||||
helper="Database should be stopped to change this settings.">
|
||||
<option value="allow" title="Allow insecure connections">allow (insecure)</option>
|
||||
<option value="prefer" title="Prefer secure connections">prefer (secure)</option>
|
||||
<option value="require" title="Require secure connections">require (secure)</option>
|
||||
@@ -164,22 +163,24 @@
|
||||
<x-forms.checkbox instantSave id="isPublic" label="Make it publicly available"
|
||||
canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
<x-forms.input placeholder="5432" disabled="{{ $isPublic }}"
|
||||
id="publicPort" label="Public Port" canGate="update" :canResource="$database" />
|
||||
<x-forms.input placeholder="5432" disabled="{{ $isPublic }}" id="publicPort"
|
||||
label="Public Port" canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.textarea label="Custom PostgreSQL Configuration" rows="10"
|
||||
id="postgresConf" canGate="update" :canResource="$database" />
|
||||
<x-forms.textarea label="Custom PostgreSQL Configuration" rows="10" id="postgresConf"
|
||||
canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="flex flex-col gap-4 pt-4">
|
||||
<h3>Advanced</h3>
|
||||
<div class="flex flex-col">
|
||||
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."
|
||||
instantSave="instantSaveAdvanced" id="isLogDrainEnabled" label="Drain Logs"
|
||||
canGate="update" :canResource="$database" />
|
||||
instantSave="instantSaveAdvanced" id="isLogDrainEnabled" label="Drain Logs" canGate="update"
|
||||
:canResource="$database" />
|
||||
</div>
|
||||
|
||||
<div class="pb-16">
|
||||
|
||||
@@ -70,32 +70,32 @@
|
||||
</div>
|
||||
<div class="text-gray-600 dark:text-gray-400 text-sm">
|
||||
@if ($backup->latest_log)
|
||||
Started:
|
||||
{{ formatDateInServerTimezone(data_get($backup->latest_log, 'created_at'), $backup->server()) }}
|
||||
@if (data_get($backup->latest_log, 'status') !== 'running')
|
||||
<br>Ended:
|
||||
{{ formatDateInServerTimezone(data_get($backup->latest_log, 'finished_at'), $backup->server()) }}
|
||||
<br>Duration:
|
||||
{{ calculateDuration(data_get($backup->latest_log, 'created_at'), data_get($backup->latest_log, 'finished_at')) }}
|
||||
<br>Finished
|
||||
{{ \Carbon\Carbon::parse(data_get($backup->latest_log, 'finished_at'))->diffForHumans() }}
|
||||
@endif
|
||||
@if ($backup->save_s3)
|
||||
<br>S3 Storage: Enabled
|
||||
@if (data_get($backup->latest_log, 'status') === 'running')
|
||||
<span title="Started: {{ formatDateInServerTimezone(data_get($backup->latest_log, 'created_at'), $backup->server()) }}">
|
||||
Running for {{ calculateDuration(data_get($backup->latest_log, 'created_at'), now()) }}
|
||||
</span>
|
||||
@else
|
||||
<span title="Started: {{ formatDateInServerTimezone(data_get($backup->latest_log, 'created_at'), $backup->server()) }} Ended: {{ formatDateInServerTimezone(data_get($backup->latest_log, 'finished_at'), $backup->server()) }}">
|
||||
{{ \Carbon\Carbon::parse(data_get($backup->latest_log, 'finished_at'))->diffForHumans() }}
|
||||
({{ calculateDuration(data_get($backup->latest_log, 'created_at'), data_get($backup->latest_log, 'finished_at')) }})
|
||||
• {{ \Carbon\Carbon::parse(data_get($backup->latest_log, 'finished_at'))->format('M j, H:i') }}
|
||||
</span>
|
||||
@endif
|
||||
@if (data_get($backup->latest_log, 'status') === 'success')
|
||||
@php
|
||||
$size = data_get($backup->latest_log, 'size', 0);
|
||||
$sizeFormatted =
|
||||
$size > 0 ? number_format($size / 1024 / 1024, 2) . ' MB' : 'Unknown';
|
||||
@endphp
|
||||
<br>Last Backup Size: {{ $sizeFormatted }}
|
||||
@if ($size > 0)
|
||||
• Size: {{ formatBytes($size) }}
|
||||
@endif
|
||||
@endif
|
||||
@if ($backup->save_s3)
|
||||
• S3: Enabled
|
||||
@endif
|
||||
@else
|
||||
Last Run: Never
|
||||
<br>Total Executions: 0
|
||||
Last Run: Never • Total Executions: 0
|
||||
@if ($backup->save_s3)
|
||||
<br>S3 Storage: Enabled
|
||||
• S3: Enabled
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@@ -154,27 +154,36 @@
|
||||
</div>
|
||||
<div class="text-gray-600 dark:text-gray-400 text-sm">
|
||||
@if ($backup->latest_log)
|
||||
Started:
|
||||
{{ formatDateInServerTimezone(data_get($backup->latest_log, 'created_at'), $backup->server()) }}
|
||||
@if (data_get($backup->latest_log, 'status') !== 'running')
|
||||
<br>Ended:
|
||||
{{ formatDateInServerTimezone(data_get($backup->latest_log, 'finished_at'), $backup->server()) }}
|
||||
<br>Duration:
|
||||
{{ calculateDuration(data_get($backup->latest_log, 'created_at'), data_get($backup->latest_log, 'finished_at')) }}
|
||||
<br>Finished
|
||||
{{ \Carbon\Carbon::parse(data_get($backup->latest_log, 'finished_at'))->diffForHumans() }}
|
||||
@if (data_get($backup->latest_log, 'status') === 'running')
|
||||
<span title="Started: {{ formatDateInServerTimezone(data_get($backup->latest_log, 'created_at'), $backup->server()) }}">
|
||||
Running for {{ calculateDuration(data_get($backup->latest_log, 'created_at'), now()) }}
|
||||
</span>
|
||||
@else
|
||||
<span title="Started: {{ formatDateInServerTimezone(data_get($backup->latest_log, 'created_at'), $backup->server()) }} Ended: {{ formatDateInServerTimezone(data_get($backup->latest_log, 'finished_at'), $backup->server()) }}">
|
||||
{{ \Carbon\Carbon::parse(data_get($backup->latest_log, 'finished_at'))->diffForHumans() }}
|
||||
({{ calculateDuration(data_get($backup->latest_log, 'created_at'), data_get($backup->latest_log, 'finished_at')) }})
|
||||
• {{ \Carbon\Carbon::parse(data_get($backup->latest_log, 'finished_at'))->format('M j, H:i') }}
|
||||
</span>
|
||||
@endif
|
||||
@if (data_get($backup->latest_log, 'status') === 'success')
|
||||
@php
|
||||
$size = data_get($backup->latest_log, 'size', 0);
|
||||
@endphp
|
||||
@if ($size > 0)
|
||||
• Size: {{ formatBytes($size) }}
|
||||
@endif
|
||||
@endif
|
||||
<br><br>Total Executions: {{ $backup->executions()->count() }}
|
||||
@if ($backup->save_s3)
|
||||
<br>S3 Storage: Enabled
|
||||
• S3: Enabled
|
||||
@endif
|
||||
<br>Total Executions: {{ $backup->executions()->count() }}
|
||||
@php
|
||||
$successCount = $backup->executions()->where('status', 'success')->count();
|
||||
$totalCount = $backup->executions()->count();
|
||||
$successRate = $totalCount > 0 ? round(($successCount / $totalCount) * 100) : 0;
|
||||
@endphp
|
||||
@if ($totalCount > 0)
|
||||
<br>Success Rate: <span @class([
|
||||
• Success Rate: <span @class([
|
||||
'font-medium',
|
||||
'text-green-600' => $successRate >= 80,
|
||||
'text-yellow-600' => $successRate >= 50 && $successRate < 80,
|
||||
@@ -182,19 +191,10 @@
|
||||
])>{{ $successRate }}%</span>
|
||||
({{ $successCount }}/{{ $totalCount }})
|
||||
@endif
|
||||
@if (data_get($backup->latest_log, 'status') === 'success')
|
||||
@php
|
||||
$size = data_get($backup->latest_log, 'size', 0);
|
||||
$sizeFormatted =
|
||||
$size > 0 ? number_format($size / 1024 / 1024, 2) . ' MB' : 'Unknown';
|
||||
@endphp
|
||||
<br>Last Backup Size: {{ $sizeFormatted }}
|
||||
@endif
|
||||
@else
|
||||
Last Run: Never
|
||||
<br>Total Executions: 0
|
||||
Last Run: Never • Total Executions: 0
|
||||
@if ($backup->save_s3)
|
||||
<br>S3 Storage: Enabled
|
||||
• S3: Enabled
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
<div class="max-w-full px-4 truncate box-description" x-text="item.description"></div>
|
||||
<div class="max-w-full px-4 truncate box-description" x-text="item.fqdn"></div>
|
||||
<template x-if="item.server_status == false">
|
||||
<div class="px-4 text-xs font-bold text-error">The underlying server has problems
|
||||
<div class="px-4 text-xs font-bold text-error">Server is unreachable or misconfigured
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -167,7 +167,7 @@
|
||||
<div class="max-w-full px-4 truncate box-description" x-text="item.description"></div>
|
||||
<div class="max-w-full px-4 truncate box-description" x-text="item.fqdn"></div>
|
||||
<template x-if="item.server_status == false">
|
||||
<div class="px-4 text-xs font-bold text-error">The underlying server has problems
|
||||
<div class="px-4 text-xs font-bold text-error">Server is unreachable or misconfigured
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -216,7 +216,7 @@
|
||||
<div class="max-w-full px-4 truncate box-description" x-text="item.description"></div>
|
||||
<div class="max-w-full px-4 truncate box-description" x-text="item.fqdn"></div>
|
||||
<template x-if="item.server_status == false">
|
||||
<div class="px-4 text-xs font-bold text-error">The underlying server has problems
|
||||
<div class="px-4 text-xs font-bold text-error">Server is unreachable or misconfigured
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
@if ($application->fqdn)
|
||||
<span class="flex gap-1 text-xs">{{ Str::limit($application->fqdn, 60) }}
|
||||
@can('update', $service)
|
||||
<x-modal-input title="Edit Domains" :closeOutside="false">
|
||||
<x-modal-input title="Edit Domains" :closeOutside="false" minWidth="32rem" maxWidth="40rem">
|
||||
<x-slot:content>
|
||||
<span class="cursor-pointer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -90,7 +90,7 @@
|
||||
@endcan
|
||||
</span>
|
||||
@endif
|
||||
<div class="pt-2 text-xs">{{ $application->status }}</div>
|
||||
<div class="pt-2 text-xs">{{ formatContainerStatus($application->status) }}</div>
|
||||
</div>
|
||||
<div class="flex items-center px-4">
|
||||
<a class="mx-4 text-xs font-bold hover:underline"
|
||||
@@ -139,7 +139,7 @@
|
||||
@if ($database->description)
|
||||
<span class="text-xs">{{ Str::limit($database->description, 60) }}</span>
|
||||
@endif
|
||||
<div class="text-xs">{{ $database->status }}</div>
|
||||
<div class="text-xs">{{ formatContainerStatus($database->status) }}</div>
|
||||
</div>
|
||||
<div class="flex items-center px-4">
|
||||
@if ($database->isBackupSolutionAvailable() || $database->is_migrated)
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<div class="w-full">
|
||||
<form wire:submit.prevent='submit' class="flex flex-col w-full gap-2">
|
||||
<div class="pb-2">Note: If a service has a defined port, do not delete it. <br>If you want to use your custom
|
||||
domain, you can add it with a port.</div>
|
||||
@if($requiredPort)
|
||||
<x-callout type="warning" title="Required Port: {{ $requiredPort }}" class="mb-2">
|
||||
This service requires port <strong>{{ $requiredPort }}</strong> to function correctly. All domains must include this port number (or any other port if you know what you're doing).
|
||||
<br><br>
|
||||
<strong>Example:</strong> http://app.coolify.io:{{ $requiredPort }}
|
||||
</x-callout>
|
||||
@endif
|
||||
|
||||
<x-forms.input canGate="update" :canResource="$application" placeholder="https://app.coolify.io" label="Domains"
|
||||
id="fqdn"
|
||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
||||
@@ -18,4 +24,61 @@
|
||||
</ul>
|
||||
</x-slot:consequences>
|
||||
</x-domain-conflict-modal>
|
||||
|
||||
@if ($showPortWarningModal)
|
||||
<div x-data="{ modalOpen: true }" x-init="$nextTick(() => { modalOpen = true })"
|
||||
@keydown.escape.window="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
:class="{ 'z-40': modalOpen }" class="relative w-auto h-auto">
|
||||
<template x-teleport="body">
|
||||
<div x-show="modalOpen"
|
||||
class="fixed top-0 lg:pt-10 left-0 z-99 flex items-start justify-center w-screen h-screen" x-cloak>
|
||||
<div x-show="modalOpen" class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
|
||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" 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 min-w-full lg:min-w-[36rem] max-w-[48rem] bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300">
|
||||
<div class="flex justify-between items-center pb-3">
|
||||
<h2 class="pr-8 font-bold">Remove Required Port?</h2>
|
||||
<button @click="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
class="flex absolute top-2 right-2 justify-center items-center w-8 h-8 rounded-full dark:text-white hover:bg-coolgray-300">
|
||||
<svg class="w-6 h-6" 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 w-auto">
|
||||
<x-callout type="warning" title="Port Requirement Warning" class="mb-4">
|
||||
This service requires port <strong>{{ $requiredPort }}</strong> to function correctly.
|
||||
One or more of your domains are missing a port number.
|
||||
</x-callout>
|
||||
|
||||
<x-callout type="danger" title="What will happen if you continue?" class="mb-4">
|
||||
<ul class="mt-2 ml-4 list-disc">
|
||||
<li>The service may become unreachable</li>
|
||||
<li>The proxy may not be able to route traffic correctly</li>
|
||||
<li>Environment variables may not be generated properly</li>
|
||||
<li>The service may fail to start or function</li>
|
||||
</ul>
|
||||
</x-callout>
|
||||
|
||||
<div class="flex flex-wrap gap-2 justify-between mt-4">
|
||||
<x-forms.button @click="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
class="w-auto dark:bg-coolgray-200 dark:hover:bg-coolgray-300">
|
||||
Cancel - Keep Port
|
||||
</x-forms.button>
|
||||
<x-forms.button wire:click="confirmRemovePort" @click="modalOpen = false" class="w-auto"
|
||||
isError>
|
||||
I understand, remove port anyway
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<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="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>
|
||||
|
||||
@@ -22,6 +22,14 @@
|
||||
@endcan
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
@if($requiredPort && !$application->serviceType()?->contains(str($application->image)->before(':')))
|
||||
<x-callout type="warning" title="Required Port: {{ $requiredPort }}" class="mb-2">
|
||||
This service requires port <strong>{{ $requiredPort }}</strong> to function correctly. All domains must include this port number (or any other port if you know what you're doing).
|
||||
<br><br>
|
||||
<strong>Example:</strong> http://app.coolify.io:{{ $requiredPort }}
|
||||
</x-callout>
|
||||
@endif
|
||||
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$application" label="Name" id="humanName"
|
||||
placeholder="Human readable name"></x-forms.input>
|
||||
@@ -68,9 +76,9 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<x-domain-conflict-modal
|
||||
:conflicts="$domainConflicts"
|
||||
:showModal="$showDomainConflictModal"
|
||||
<x-domain-conflict-modal
|
||||
:conflicts="$domainConflicts"
|
||||
:showModal="$showDomainConflictModal"
|
||||
confirmAction="confirmDomainUsage">
|
||||
<x-slot:consequences>
|
||||
<ul class="mt-2 ml-4 list-disc">
|
||||
@@ -81,4 +89,61 @@
|
||||
</ul>
|
||||
</x-slot:consequences>
|
||||
</x-domain-conflict-modal>
|
||||
|
||||
@if ($showPortWarningModal)
|
||||
<div x-data="{ modalOpen: true }" x-init="$nextTick(() => { modalOpen = true })"
|
||||
@keydown.escape.window="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
:class="{ 'z-40': modalOpen }" class="relative w-auto h-auto">
|
||||
<template x-teleport="body">
|
||||
<div x-show="modalOpen"
|
||||
class="fixed top-0 lg:pt-10 left-0 z-99 flex items-start justify-center w-screen h-screen" x-cloak>
|
||||
<div x-show="modalOpen" class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
|
||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" 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 min-w-full lg:min-w-[36rem] max-w-[48rem] bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300">
|
||||
<div class="flex justify-between items-center pb-3">
|
||||
<h2 class="pr-8 font-bold">Remove Required Port?</h2>
|
||||
<button @click="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
class="flex absolute top-2 right-2 justify-center items-center w-8 h-8 rounded-full dark:text-white hover:bg-coolgray-300">
|
||||
<svg class="w-6 h-6" 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 w-auto">
|
||||
<x-callout type="warning" title="Port Requirement Warning" class="mb-4">
|
||||
This service requires port <strong>{{ $requiredPort }}</strong> to function correctly.
|
||||
One or more of your domains are missing a port number.
|
||||
</x-callout>
|
||||
|
||||
<x-callout type="danger" title="What will happen if you continue?" class="mb-4">
|
||||
<ul class="mt-2 ml-4 list-disc">
|
||||
<li>The service may become unreachable</li>
|
||||
<li>The proxy may not be able to route traffic correctly</li>
|
||||
<li>Environment variables may not be generated properly</li>
|
||||
<li>The service may fail to start or function</li>
|
||||
</ul>
|
||||
</x-callout>
|
||||
|
||||
<div class="flex flex-wrap gap-2 justify-between mt-4">
|
||||
<x-forms.button @click="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
class="w-auto dark:bg-coolgray-200 dark:hover:bg-coolgray-300">
|
||||
Cancel - Keep Port
|
||||
</x-forms.button>
|
||||
<x-forms.button wire:click="confirmRemovePort" @click="modalOpen = false" class="w-auto"
|
||||
isError>
|
||||
I understand, remove port anyway
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
<form class="flex flex-col w-full gap-2 rounded-sm" wire:submit='submit'>
|
||||
<x-forms.input placeholder="NODE_ENV" id="key" label="Name" required />
|
||||
<x-forms.textarea x-show="$wire.is_multiline === true" x-cloak id="value" label="Value" required />
|
||||
<x-forms.input x-show="$wire.is_multiline === false" x-cloak placeholder="production" id="value"
|
||||
x-bind:label="$wire.is_multiline === false && 'Value'" required />
|
||||
@if ($is_multiline)
|
||||
<x-forms.textarea id="value" label="Value" required />
|
||||
@else
|
||||
<x-forms.env-var-input placeholder="production" id="value" label="Value" required
|
||||
:availableVars="$shared ? [] : $this->availableSharedVariables"
|
||||
:projectUuid="data_get($parameters, 'project_uuid')"
|
||||
:environmentUuid="data_get($parameters, 'environment_uuid')" />
|
||||
@endif
|
||||
|
||||
@if (!$shared && !$is_multiline)
|
||||
<div class="text-xs text-neutral-500 dark:text-neutral-400 -mt-1">
|
||||
Tip: Type <span class="font-mono dark:text-warning text-coollabs">{{</span> to reference a shared environment
|
||||
variable
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (!$shared)
|
||||
<x-forms.checkbox id="is_buildtime"
|
||||
@@ -22,4 +34,4 @@
|
||||
<x-forms.button type="submit" @click="slideOverOpen=false">
|
||||
Save
|
||||
</x-forms.button>
|
||||
</form>
|
||||
</form>
|
||||
@@ -4,6 +4,9 @@
|
||||
<x-forms.input placeholder="0 0 * * * or daily"
|
||||
helper="You can use every_minute, hourly, daily, weekly, monthly, yearly or a cron expression." id="frequency"
|
||||
label="Frequency" />
|
||||
<x-forms.input type="number" placeholder="300" id="timeout"
|
||||
helper="Maximum execution time in seconds (60-3600). Default is 300 seconds (5 minutes)."
|
||||
label="Timeout (seconds)" />
|
||||
@if ($type === 'application')
|
||||
@if ($containerNames->count() > 1)
|
||||
<x-forms.select id="container" label="Container name">
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
<x-forms.input placeholder="Name" id="name" label="Name" required />
|
||||
<x-forms.input placeholder="php artisan schedule:run" id="command" label="Command" required />
|
||||
<x-forms.input placeholder="0 0 * * * or daily" id="frequency" label="Frequency" required />
|
||||
<x-forms.input type="number" placeholder="300" id="timeout"
|
||||
helper="Maximum execution time in seconds (60-3600)." label="Timeout (seconds)" required />
|
||||
@if ($type === 'application')
|
||||
<x-forms.input placeholder="php"
|
||||
helper="You can leave this empty if your resource only has one container." id="container"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="pb-6">
|
||||
<x-slide-over @startproxy.window="slideOverOpen = true" fullScreen>
|
||||
<x-slide-over @startproxy.window="slideOverOpen = true" fullScreen closeWithX>
|
||||
<x-slot:title>Proxy Startup Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:activity-monitor header="Logs" fullHeight />
|
||||
@@ -56,112 +56,106 @@
|
||||
<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">
|
||||
<a class="{{ request()->routeIs('server.show') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('server.show', [
|
||||
'server_uuid' => data_get($server, 'uuid'),
|
||||
]) }}">
|
||||
<a class="{{ request()->routeIs('server.show') ? 'dark:text-white' : '' }}" href="{{ route('server.show', [
|
||||
'server_uuid' => data_get($server, 'uuid'),
|
||||
]) }}">
|
||||
Configuration
|
||||
</a>
|
||||
|
||||
@if (!$server->isSwarmWorker() && !$server->settings->is_build_server)
|
||||
<a class="{{ request()->routeIs('server.proxy') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('server.proxy', [
|
||||
'server_uuid' => data_get($server, 'uuid'),
|
||||
]) }}">
|
||||
Proxy
|
||||
</a>
|
||||
@endif
|
||||
<a class="{{ request()->routeIs('server.resources') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('server.resources', [
|
||||
<a class="{{ request()->routeIs('server.proxy') ? 'dark:text-white' : '' }} flex items-center gap-1" href="{{ route('server.proxy', [
|
||||
'server_uuid' => data_get($server, 'uuid'),
|
||||
]) }}">
|
||||
Proxy
|
||||
@if ($this->hasTraefikOutdated)
|
||||
<svg class="w-4 h-4 text-warning" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor"
|
||||
d="M236.8 188.09L149.35 36.22a24.76 24.76 0 0 0-42.7 0L19.2 188.09a23.51 23.51 0 0 0 0 23.72A24.35 24.35 0 0 0 40.55 224h174.9a24.35 24.35 0 0 0 21.33-12.19a23.51 23.51 0 0 0 .02-23.72m-13.87 15.71a8.5 8.5 0 0 1-7.48 4.2H40.55a8.5 8.5 0 0 1-7.48-4.2a7.59 7.59 0 0 1 0-7.72l87.45-151.87a8.75 8.75 0 0 1 15 0l87.45 151.87a7.59 7.59 0 0 1-.04 7.72M120 144v-40a8 8 0 0 1 16 0v40a8 8 0 0 1-16 0m20 36a12 12 0 1 1-12-12a12 12 0 0 1 12 12" />
|
||||
</svg>
|
||||
@endif
|
||||
</a>
|
||||
@endif
|
||||
<a class="{{ request()->routeIs('server.resources') ? 'dark:text-white' : '' }}" href="{{ route('server.resources', [
|
||||
'server_uuid' => data_get($server, 'uuid'),
|
||||
]) }}">
|
||||
Resources
|
||||
</a>
|
||||
@can('canAccessTerminal')
|
||||
<a class="{{ request()->routeIs('server.command') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('server.command', [
|
||||
'server_uuid' => data_get($server, 'uuid'),
|
||||
]) }}">
|
||||
Terminal
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('server.command') ? 'dark:text-white' : '' }}" href="{{ route('server.command', [
|
||||
'server_uuid' => data_get($server, 'uuid'),
|
||||
]) }}">
|
||||
Terminal
|
||||
</a>
|
||||
@endcan
|
||||
@can('update', $server)
|
||||
<a class="{{ request()->routeIs('server.security.patches') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('server.security.patches', [
|
||||
'server_uuid' => data_get($server, 'uuid'),
|
||||
]) }}">
|
||||
Security
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('server.security.patches') ? 'dark:text-white' : '' }}" href="{{ route('server.security.patches', [
|
||||
'server_uuid' => data_get($server, 'uuid'),
|
||||
]) }}">
|
||||
Security
|
||||
</a>
|
||||
@endcan
|
||||
</nav>
|
||||
<div class="order-first sm:order-last">
|
||||
<div>
|
||||
@if ($server->proxySet())
|
||||
<x-slide-over fullScreen @startproxy.window="slideOverOpen = true">
|
||||
<x-slot:title>Proxy Status</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:activity-monitor header="Logs" />
|
||||
</x-slot:content>
|
||||
</x-slide-over>
|
||||
@if ($proxyStatus === 'running')
|
||||
<div class="flex gap-2">
|
||||
<div class="mt-1" wire:loading wire:target="loadProxyConfiguration">
|
||||
<x-loading text="Checking Traefik dashboard" />
|
||||
</div>
|
||||
@if ($traefikDashboardAvailable)
|
||||
<button>
|
||||
<a target="_blank" href="http://{{ $serverIp }}:8080">
|
||||
Traefik Dashboard
|
||||
<x-external-link />
|
||||
</a>
|
||||
</button>
|
||||
@endif
|
||||
<x-modal-confirmation title="Confirm Proxy Restart?" buttonTitle="Restart Proxy"
|
||||
submitAction="restart" :actions="[
|
||||
'This proxy will be stopped and started again.',
|
||||
'All resources hosted on coolify will be unavailable during the restart.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step2ButtonText="Restart Proxy" :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">
|
||||
<div class="flex gap-2">
|
||||
<div class="mt-1" wire:loading wire:target="loadProxyConfiguration">
|
||||
<x-loading text="Checking Traefik dashboard" />
|
||||
</div>
|
||||
@if ($traefikDashboardAvailable)
|
||||
<button>
|
||||
<a target="_blank" href="http://{{ $serverIp }}:8080">
|
||||
Traefik Dashboard
|
||||
<x-external-link />
|
||||
</a>
|
||||
</button>
|
||||
@endif
|
||||
<x-modal-confirmation title="Confirm Proxy Restart?" buttonTitle="Restart Proxy"
|
||||
submitAction="restart" :actions="[
|
||||
'This proxy will be stopped and started again.',
|
||||
'All resources hosted on coolify will be unavailable during the restart.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Restart Proxy"
|
||||
: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 Proxy
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Proxy Stopping?" buttonTitle="Stop Proxy"
|
||||
submitAction="stop(true)" :actions="[
|
||||
'The coolify proxy will be stopped.',
|
||||
'All resources hosted on coolify will be unavailable.',
|
||||
]" :confirmWithText="false"
|
||||
:confirmWithPassword="false" step2ButtonText="Stop Proxy" :dispatchEvent="true"
|
||||
dispatchEventType="stopEvent">
|
||||
<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="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 Proxy
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Proxy Stopping?" buttonTitle="Stop Proxy"
|
||||
submitAction="stop(true)" :actions="[
|
||||
'The coolify proxy will be stopped.',
|
||||
'All resources hosted on coolify will be unavailable.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step2ButtonText="Stop Proxy" :dispatchEvent="true" dispatchEventType="stopEvent">
|
||||
<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 Proxy
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
</div>
|
||||
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 Proxy
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
</div>
|
||||
@else
|
||||
<button @click="$wire.dispatch('checkProxyEvent')" class="gap-2 button">
|
||||
<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">
|
||||
<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>
|
||||
@@ -170,29 +164,29 @@
|
||||
@endif
|
||||
@endif
|
||||
@script
|
||||
<script>
|
||||
$wire.$on('checkProxyEvent', () => {
|
||||
try {
|
||||
$wire.$call('checkProxy');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
$wire.$dispatch('error', 'Failed to check proxy status. Please try again.');
|
||||
}
|
||||
});
|
||||
$wire.$on('restartEvent', () => {
|
||||
$wire.$dispatch('info', 'Initiating proxy restart.');
|
||||
$wire.$call('restart');
|
||||
});
|
||||
$wire.$on('startProxy', () => {
|
||||
window.dispatchEvent(new CustomEvent('startproxy'))
|
||||
$wire.$call('startProxy');
|
||||
});
|
||||
$wire.$on('stopEvent', () => {
|
||||
$wire.$call('stop');
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
$wire.$on('checkProxyEvent', () => {
|
||||
try {
|
||||
$wire.$call('checkProxy');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
$wire.$dispatch('error', 'Failed to check proxy status. Please try again.');
|
||||
}
|
||||
});
|
||||
$wire.$on('restartEvent', () => {
|
||||
window.dispatchEvent(new CustomEvent('startproxy'))
|
||||
$wire.$call('restart');
|
||||
});
|
||||
$wire.$on('startProxy', () => {
|
||||
window.dispatchEvent(new CustomEvent('startproxy'))
|
||||
$wire.$call('startProxy');
|
||||
});
|
||||
$wire.$on('stopEvent', () => {
|
||||
$wire.$call('stop');
|
||||
});
|
||||
</script>
|
||||
@endscript
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -21,7 +21,15 @@
|
||||
@endif
|
||||
<x-forms.button canGate="update" :canResource="$server" type="submit">Save</x-forms.button>
|
||||
</div>
|
||||
<div class="subtitle">Configure your proxy settings and advanced options.</div>
|
||||
<div class="pb-4">Configure your proxy settings and advanced options.</div>
|
||||
@if (
|
||||
$server->proxy->last_applied_settings &&
|
||||
$server->proxy->last_saved_settings !== $server->proxy->last_applied_settings)
|
||||
<x-callout type="warning" title="Configuration Out of Sync" class="my-4">
|
||||
The saved proxy configuration differs from the currently running configuration. Restart the
|
||||
proxy to apply your changes.
|
||||
</x-callout>
|
||||
@endif
|
||||
<h3>Advanced</h3>
|
||||
<div class="pb-6 w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$server"
|
||||
@@ -43,32 +51,89 @@
|
||||
: 'Caddy (Coolify Proxy)';
|
||||
@endphp
|
||||
@if ($server->proxyType() === ProxyTypes::TRAEFIK->value || $server->proxyType() === 'CADDY')
|
||||
<div class="flex items-center gap-2">
|
||||
<h3>{{ $proxyTitle }}</h3>
|
||||
@if ($proxySettings)
|
||||
<div @if($server->proxyType() === ProxyTypes::TRAEFIK->value) x-data="{ traefikWarningsDismissed: localStorage.getItem('callout-dismissed-traefik-warnings-{{ $server->id }}') === 'true' }" @endif>
|
||||
<div class="flex items-center gap-2">
|
||||
<h3>{{ $proxyTitle }}</h3>
|
||||
@can('update', $server)
|
||||
<x-modal-confirmation title="Reset Proxy Configuration?"
|
||||
buttonTitle="Reset Configuration" submitAction="resetProxyConfiguration"
|
||||
:actions="[
|
||||
'Reset proxy configuration to default settings',
|
||||
'All custom configurations will be lost',
|
||||
'Custom ports and entrypoints will be removed',
|
||||
]" confirmationText="{{ $server->name }}"
|
||||
confirmationLabel="Please confirm by entering the server name below"
|
||||
shortConfirmationLabel="Server Name" step2ButtonText="Reset Configuration"
|
||||
:confirmWithPassword="false" :confirmWithText="true">
|
||||
</x-modal-confirmation>
|
||||
<div wire:loading wire:target="loadProxyConfiguration">
|
||||
<x-forms.button disabled>Reset Configuration</x-forms.button>
|
||||
</div>
|
||||
<div wire:loading.remove wire:target="loadProxyConfiguration">
|
||||
@if ($proxySettings)
|
||||
<x-modal-confirmation title="Reset Proxy Configuration?"
|
||||
buttonTitle="Reset Configuration" submitAction="resetProxyConfiguration"
|
||||
:actions="[
|
||||
'Reset proxy configuration to default settings',
|
||||
'All custom configurations will be lost',
|
||||
'Custom ports and entrypoints will be removed',
|
||||
]" confirmationText="{{ $server->name }}"
|
||||
confirmationLabel="Please confirm by entering the server name below"
|
||||
shortConfirmationLabel="Server Name" step2ButtonText="Reset Configuration"
|
||||
:confirmWithPassword="false" :confirmWithText="true">
|
||||
</x-modal-confirmation>
|
||||
@endif
|
||||
</div>
|
||||
@endcan
|
||||
@if ($server->proxyType() === ProxyTypes::TRAEFIK->value)
|
||||
<button type="button" x-show="traefikWarningsDismissed"
|
||||
@click="traefikWarningsDismissed = false; localStorage.removeItem('callout-dismissed-traefik-warnings-{{ $server->id }}')"
|
||||
class="p-1.5 rounded hover:bg-yellow-100 dark:hover:bg-yellow-900/30 transition-colors"
|
||||
title="Show Traefik warnings">
|
||||
<svg class="w-4 h-4 text-yellow-600 dark:text-yellow-400" 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>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
@if ($server->proxyType() === ProxyTypes::TRAEFIK->value)
|
||||
<div x-show="!traefikWarningsDismissed"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100 translate-y-0"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2">
|
||||
@if ($server->detected_traefik_version === 'latest')
|
||||
<x-callout dismissible onDismiss="traefikWarningsDismissed = true; localStorage.setItem('callout-dismissed-traefik-warnings-{{ $server->id }}', 'true')" type="warning" title="Using 'latest' Traefik Tag" class="my-4">
|
||||
Your proxy container is running the <span class="font-mono">latest</span> tag. While
|
||||
this ensures you always have the newest version, it may introduce unexpected breaking
|
||||
changes.
|
||||
<br><br>
|
||||
<strong>Recommendation:</strong> Pin to a specific version (e.g., <span
|
||||
class="font-mono">traefik:{{ $this->latestTraefikVersion }}</span>) to ensure
|
||||
stability and predictable updates.
|
||||
</x-callout>
|
||||
@elseif($this->isTraefikOutdated)
|
||||
<x-callout dismissible onDismiss="traefikWarningsDismissed = true; localStorage.setItem('callout-dismissed-traefik-warnings-{{ $server->id }}', 'true')" type="warning" title="Traefik Patch Update Available" class="my-4">
|
||||
Your Traefik proxy container is running version <span
|
||||
class="font-mono">v{{ $server->detected_traefik_version }}</span>, but version <span
|
||||
class="font-mono">{{ $this->latestTraefikVersion }}</span> is available.
|
||||
<br><br>
|
||||
<strong>Recommendation:</strong> Update to the latest patch version for security fixes
|
||||
and
|
||||
bug fixes. Please test in a non-production environment first.
|
||||
</x-callout>
|
||||
@endif
|
||||
@if ($this->newerTraefikBranchAvailable)
|
||||
<x-callout dismissible onDismiss="traefikWarningsDismissed = true; localStorage.setItem('callout-dismissed-traefik-warnings-{{ $server->id }}', 'true')" type="info" title="New Minor Traefik Version Available" class="my-4">
|
||||
A new minor version of Traefik is available: <span
|
||||
class="font-mono">{{ $this->newerTraefikBranchAvailable }}</span>
|
||||
<br><br>
|
||||
You are currently running <span class="font-mono">v{{ $server->detected_traefik_version }}</span>.
|
||||
Upgrading to <span class="font-mono">{{ $this->newerTraefikBranchAvailable }}</span> will give you access to new features and improvements.
|
||||
<br><br>
|
||||
<strong>Important:</strong> Before upgrading to a new minor version, please read
|
||||
the <a href="https://github.com/traefik/traefik/releases" target="_blank"
|
||||
class="underline text-white">Traefik changelog</a> to understand breaking changes
|
||||
and new features.
|
||||
<br><br>
|
||||
<strong>Recommendation:</strong> Test the upgrade in a non-production environment first.
|
||||
</x-callout>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@if (
|
||||
$server->proxy->last_applied_settings &&
|
||||
$server->proxy->last_saved_settings !== $server->proxy->last_applied_settings)
|
||||
<div class="text-red-500 ">Configuration out of sync. Restart the proxy to apply the new
|
||||
configurations.
|
||||
</div>
|
||||
@endif
|
||||
<div wire:loading wire:target="loadProxyConfiguration" class="pt-4">
|
||||
<x-loading text="Loading proxy configuration..." />
|
||||
</div>
|
||||
|
||||
@@ -52,6 +52,30 @@
|
||||
@endif
|
||||
@endif
|
||||
@if ($uptime && $supported_os_type)
|
||||
@if ($prerequisites_installed)
|
||||
<div class="flex w-64 gap-2">Prerequisites are installed: <svg class="w-5 h-5 text-success"
|
||||
viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="currentColor">
|
||||
<path
|
||||
d="m237.66 85.26l-128.4 128.4a8 8 0 0 1-11.32 0l-71.6-72a8 8 0 0 1 0-11.31l24-24a8 8 0 0 1 11.32 0l36.68 35.32a8 8 0 0 0 11.32 0l92.68-91.32a8 8 0 0 1 11.32 0l24 23.6a8 8 0 0 1 0 11.31"
|
||||
opacity=".2" />
|
||||
<path
|
||||
d="m243.28 68.24l-24-23.56a16 16 0 0 0-22.58 0L104 136l-.11-.11l-36.64-35.27a16 16 0 0 0-22.57.06l-24 24a16 16 0 0 0 0 22.61l71.62 72a16 16 0 0 0 22.63 0l128.4-128.38a16 16 0 0 0-.05-22.67M103.62 208L32 136l24-24l.11.11l36.64 35.27a16 16 0 0 0 22.52 0L208.06 56L232 79.6Z" />
|
||||
</g>
|
||||
</svg></div>
|
||||
@else
|
||||
@if ($error)
|
||||
<div class="flex w-64 gap-2">Prerequisites are installed: <svg class="w-5 h-5 text-error"
|
||||
viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor"
|
||||
d="M208.49 191.51a12 12 0 0 1-17 17L128 145l-63.51 63.49a12 12 0 0 1-17-17L111 128L47.51 64.49a12 12 0 0 1 17-17L128 111l63.51-63.52a12 12 0 0 1 17 17L145 128Z" />
|
||||
</svg></div>
|
||||
@else
|
||||
<div class="w-64"><x-loading text="Prerequisites are installed:" /></div>
|
||||
@endif
|
||||
@endif
|
||||
@endif
|
||||
@if ($uptime && $supported_os_type && $prerequisites_installed)
|
||||
@if ($docker_installed)
|
||||
<div class="flex w-64 gap-2">Docker is installed: <svg class="w-5 h-5 text-success"
|
||||
viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -120,7 +144,7 @@
|
||||
@endif
|
||||
|
||||
@endif
|
||||
<livewire:activity-monitor header="Docker Installation Logs" :showWaiting="false" />
|
||||
<livewire:activity-monitor header="{{ $installationStep }} Installation Logs" :showWaiting="false" />
|
||||
@isset($error)
|
||||
<pre class="font-bold whitespace-pre-line text-error">{!! $error !!}</pre>
|
||||
<x-forms.button canGate="update" :canResource="$server" wire:click="retry" class="mt-4">
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
<div>
|
||||
<x-slot:title>
|
||||
Settings | Coolify
|
||||
</x-slot>
|
||||
<x-settings.navbar />
|
||||
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<x-settings.sidebar activeMenu="general" />
|
||||
<form wire:submit='submit' class="flex flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>General</h2>
|
||||
<x-forms.button type="submit">
|
||||
Save
|
||||
</x-forms.button>
|
||||
</div>
|
||||
<div class="pb-4">General configuration for your Coolify instance.</div>
|
||||
</x-slot>
|
||||
<x-settings.navbar />
|
||||
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }"
|
||||
class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<x-settings.sidebar activeMenu="general" />
|
||||
<form wire:submit='submit' class="flex flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>General</h2>
|
||||
<x-forms.button canGate="update" :canResource="$settings" type="submit">
|
||||
Save
|
||||
</x-forms.button>
|
||||
</div>
|
||||
<div class="pb-4">General configuration for your Coolify instance.</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-wrap items-end gap-2">
|
||||
<div class="flex gap-2 md:flex-row flex-col w-full">
|
||||
<x-forms.input id="fqdn" label="Domain"
|
||||
helper="Enter the full domain name (FQDN) of the instance, including 'https://' if you want to secure the dashboard with HTTPS. Setting this will make the dashboard accessible via this domain, secured by HTTPS, instead of just the IP address."
|
||||
placeholder="https://coolify.yourdomain.com" />
|
||||
<x-forms.input id="instance_name" label="Name" placeholder="Coolify"
|
||||
helper="Custom name for your Coolify instance, shown in the URL." />
|
||||
<div class="w-full" x-data="{
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-wrap items-end gap-2">
|
||||
<div class="flex gap-2 md:flex-row flex-col w-full">
|
||||
<x-forms.input canGate="update" :canResource="$settings" id="fqdn" label="Domain"
|
||||
helper="Enter the full domain name (FQDN) of the instance, including 'https://' if you want to secure the dashboard with HTTPS. Setting this will make the dashboard accessible via this domain, secured by HTTPS, instead of just the IP address."
|
||||
placeholder="https://coolify.yourdomain.com" />
|
||||
<x-forms.input canGate="update" :canResource="$settings" id="instance_name" label="Name" placeholder="Coolify"
|
||||
helper="Custom name for your Coolify instance, shown in the URL." />
|
||||
<div class="w-full" x-data="{
|
||||
open: false,
|
||||
search: '{{ $settings->instance_timezone ?: '' }}',
|
||||
timezones: @js($this->timezones),
|
||||
@@ -35,63 +36,73 @@
|
||||
})
|
||||
}
|
||||
}">
|
||||
<div class="flex items-center mb-1">
|
||||
<label for="instance_timezone">Instance
|
||||
Timezone</label>
|
||||
<x-helper class="ml-2"
|
||||
helper="Timezone for the Coolify instance. This is used for the update check and automatic update frequency." />
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div class="inline-flex relative items-center w-full">
|
||||
<input autocomplete="off"
|
||||
wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300'
|
||||
wire:dirty.class="dark:focus:ring-warning dark:ring-warning" x-model="search"
|
||||
@focus="open = true" @click.away="open = false" @input="open = true"
|
||||
class="w-full input" :placeholder="placeholder" wire:model="instance_timezone">
|
||||
<svg class="absolute right-0 mr-2 w-4 h-4" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
@click="open = true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9" />
|
||||
</svg>
|
||||
<div class="flex items-center mb-1">
|
||||
<label for="instance_timezone">Instance
|
||||
Timezone</label>
|
||||
<x-helper class="ml-2"
|
||||
helper="Timezone for the Coolify instance. This is used for the update check and automatic update frequency." />
|
||||
</div>
|
||||
<div x-show="open"
|
||||
class="overflow-auto overflow-x-hidden absolute z-50 mt-1 w-full max-h-60 bg-white rounded-md border shadow-lg dark:bg-coolgray-100 dark:border-coolgray-200 scrollbar">
|
||||
<template
|
||||
x-for="timezone in timezones.filter(tz => tz.toLowerCase().includes(search.toLowerCase()))"
|
||||
:key="timezone">
|
||||
<div @click="search = timezone; open = false; $wire.set('instance_timezone', timezone); $wire.submit()"
|
||||
class="px-4 py-2 text-gray-800 cursor-pointer hover:bg-gray-100 dark:hover:bg-coolgray-300 dark:text-gray-200"
|
||||
x-text="timezone"></div>
|
||||
</template>
|
||||
<div class="relative">
|
||||
<div class="inline-flex relative items-center w-full">
|
||||
<input autocomplete="off"
|
||||
wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300'
|
||||
wire:dirty.class="dark:focus:ring-warning dark:ring-warning"
|
||||
x-model="search" @focus="open = true" @click.away="open = false"
|
||||
@input="open = true" class="w-full input" :placeholder="placeholder"
|
||||
wire:model="instance_timezone">
|
||||
<svg class="absolute right-0 mr-2 w-4 h-4" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
@click="open = true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9" />
|
||||
</svg>
|
||||
</div>
|
||||
<div x-show="open"
|
||||
class="overflow-auto overflow-x-hidden absolute z-50 mt-1 w-full max-h-60 bg-white rounded-md border shadow-lg dark:bg-coolgray-100 dark:border-coolgray-200 scrollbar">
|
||||
<template
|
||||
x-for="timezone in timezones.filter(tz => tz.toLowerCase().includes(search.toLowerCase()))"
|
||||
:key="timezone">
|
||||
<div @click="search = timezone; open = false; $wire.set('instance_timezone', timezone); $wire.submit()"
|
||||
class="px-4 py-2 text-gray-800 cursor-pointer hover:bg-gray-100 dark:hover:bg-coolgray-300 dark:text-gray-200"
|
||||
x-text="timezone"></div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 md:flex-row flex-col w-full">
|
||||
<x-forms.input id="public_ipv4" type="password" label="Instance's Public IPv4"
|
||||
helper="Enter the IPv4 address of the instance.<br><br>It is useful if you have several IPv4 addresses and Coolify could not detect the correct one."
|
||||
placeholder="1.2.3.4" autocomplete="new-password" />
|
||||
<x-forms.input id="public_ipv6" type="password" label="Instance's Public IPv6"
|
||||
helper="Enter the IPv6 address of the instance.<br><br>It is useful if you have several IPv6 addresses and Coolify could not detect the correct one."
|
||||
placeholder="2001:db8::1" autocomplete="new-password" />
|
||||
</div>
|
||||
<div class="flex gap-2 md:flex-row flex-col w-full">
|
||||
<x-forms.input canGate="update" :canResource="$settings" id="public_ipv4" type="password" label="Instance's Public IPv4"
|
||||
helper="Enter the IPv4 address of the instance.<br><br>It is useful if you have several IPv4 addresses and Coolify could not detect the correct one."
|
||||
placeholder="1.2.3.4" autocomplete="new-password" />
|
||||
<x-forms.input canGate="update" :canResource="$settings" id="public_ipv6" type="password" label="Instance's Public IPv6"
|
||||
helper="Enter the IPv6 address of the instance.<br><br>It is useful if you have several IPv6 addresses and Coolify could not detect the correct one."
|
||||
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"
|
||||
placeholder="{{ config('constants.coolify.helper_version') }}" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<x-domain-conflict-modal
|
||||
:conflicts="$domainConflicts"
|
||||
:showModal="$showDomainConflictModal"
|
||||
confirmAction="confirmDomainUsage">
|
||||
<x-slot:consequences>
|
||||
<ul class="mt-2 ml-4 list-disc">
|
||||
<li>The Coolify instance domain will conflict with existing resources</li>
|
||||
<li>SSL certificates might not work correctly</li>
|
||||
<li>Routing behavior will be unpredictable</li>
|
||||
<li>You may not be able to access the Coolify dashboard properly</li>
|
||||
</ul>
|
||||
</x-slot:consequences>
|
||||
</x-domain-conflict-modal>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<x-domain-conflict-modal :conflicts="$domainConflicts" :showModal="$showDomainConflictModal"
|
||||
confirmAction="confirmDomainUsage">
|
||||
<x-slot:consequences>
|
||||
<ul class="mt-2 ml-4 list-disc">
|
||||
<li>The Coolify instance domain will conflict with existing resources</li>
|
||||
<li>SSL certificates might not work correctly</li>
|
||||
<li>Routing behavior will be unpredictable</li>
|
||||
<li>You may not be able to access the Coolify dashboard properly</li>
|
||||
</ul>
|
||||
</x-slot:consequences>
|
||||
</x-domain-conflict-modal>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user