mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-15 12:14:07 +00:00
Merge branch 'next' into feat/healthcheck-cmd
This commit is contained in:
192 files changed
+16780
-10634
No files matched your search
+12
-2
@@ -96,7 +96,17 @@ body {
|
||||
}
|
||||
|
||||
body {
|
||||
@apply min-h-screen text-sm antialiased scrollbar;
|
||||
@apply min-h-screen text-sm antialiased scrollbar overflow-x-hidden;
|
||||
}
|
||||
|
||||
.coolify-monaco-editor {
|
||||
@apply min-w-0 w-full;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.coolify-monaco-editor .monaco-editor,
|
||||
.coolify-monaco-editor .overflow-guard {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
option {
|
||||
@@ -196,4 +206,4 @@ section {
|
||||
|
||||
.dark .log-highlight {
|
||||
background-color: rgba(234, 179, 8, 0.3);
|
||||
}
|
||||
}
|
||||
@@ -281,7 +281,7 @@
|
||||
}
|
||||
|
||||
@utility info-helper-popup {
|
||||
@apply hidden absolute z-40 text-xs rounded-sm text-neutral-700 group-hover:block dark:border-coolgray-500 border-neutral-900 dark:bg-coolgray-400 bg-neutral-200 dark:text-neutral-300 max-w-xs whitespace-normal break-words;
|
||||
@apply hidden absolute z-40 text-xs rounded-sm text-neutral-700 group-hover:block dark:border-coolgray-500 border-neutral-900 dark:bg-coolgray-400 bg-neutral-200 dark:text-neutral-300 max-w-sm whitespace-normal break-words;
|
||||
}
|
||||
|
||||
@utility buyme {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<?php
|
||||
function getOldOrLocal($key, $localValue)
|
||||
{
|
||||
return old($key) != '' ? old($key) : (app()->environment('local') ? $localValue : '');
|
||||
if (! function_exists('getOldOrLocal')) {
|
||||
function getOldOrLocal($key, $localValue)
|
||||
{
|
||||
return old($key) != '' ? old($key) : (app()->environment('local') ? $localValue : '');
|
||||
}
|
||||
}
|
||||
|
||||
$name = getOldOrLocal('name', 'test3 normal user');
|
||||
|
||||
@@ -99,8 +99,14 @@
|
||||
{{-- Unified Input Container with Tags Inside --}}
|
||||
<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'; } })"
|
||||
:style="(() => {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
const accent = isDark ? '#fcd452' : '#6b16ed';
|
||||
const border = isDark ? '#242424' : '#e5e5e5';
|
||||
return focused
|
||||
? 'box-shadow: inset 4px 0 0 ' + accent + ', inset 0 0 0 2px ' + border + ';'
|
||||
: 'box-shadow: inset 4px 0 0 transparent, inset 0 0 0 2px ' + border + ';';
|
||||
})()"
|
||||
:class="{
|
||||
'opacity-50': {{ $disabled ? 'true' : 'false' }}
|
||||
}" wire:loading.class="opacity-50"
|
||||
@@ -225,8 +231,14 @@
|
||||
{{-- Input Container --}}
|
||||
<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'; } })"
|
||||
:style="(() => {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
const accent = isDark ? '#fcd452' : '#6b16ed';
|
||||
const border = isDark ? '#242424' : '#e5e5e5';
|
||||
return focused
|
||||
? 'box-shadow: inset 4px 0 0 ' + accent + ', inset 0 0 0 2px ' + border + ';'
|
||||
: 'box-shadow: inset 4px 0 0 transparent, inset 0 0 0 2px ' + border + ';';
|
||||
})()"
|
||||
:class="{
|
||||
'opacity-50': {{ $disabled ? 'true' : 'false' }}
|
||||
}" 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]">
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
'action' => 'delete',
|
||||
'content' => null,
|
||||
'closeOutside' => true,
|
||||
'minWidth' => '36rem',
|
||||
'maxWidth' => '48rem',
|
||||
'isFullWidth' => false,
|
||||
])
|
||||
|
||||
@@ -16,7 +14,6 @@
|
||||
$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"
|
||||
@@ -52,7 +49,7 @@
|
||||
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 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">
|
||||
class="relative w-full lg:w-auto lg:min-w-2xl lg:max-w-4xl 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"
|
||||
@@ -63,7 +60,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative flex items-center justify-center w-auto overflow-y-auto px-6 pb-6" style="-webkit-overflow-scrolling: touch;">
|
||||
<div class="relative flex items-center justify-center w-auto px-6 pb-6">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<h1>Notifications</h1>
|
||||
<div class="subtitle">Get notified about your infrastructure.</div>
|
||||
<div class="navbar-main">
|
||||
<nav class="flex items-center gap-6 min-h-10">
|
||||
<nav class="flex items-center gap-3.5 min-h-10">
|
||||
<a class="{{ request()->routeIs('notifications.email') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('notifications.email') }}">
|
||||
<button>Email</button>
|
||||
|
||||
@@ -2,12 +2,28 @@
|
||||
'lastDeploymentInfo' => null,
|
||||
'lastDeploymentLink' => null,
|
||||
'resource' => null,
|
||||
'projects' => null,
|
||||
'environments' => null,
|
||||
])
|
||||
@php
|
||||
$projects = auth()->user()->currentTeam()->projects()->get();
|
||||
$environments = $resource->environment->project
|
||||
use App\Models\Project;
|
||||
|
||||
// Use passed props if available, otherwise query (backwards compatible)
|
||||
$projects = $projects ?? Project::ownedByCurrentTeamCached();
|
||||
$environments = $environments ?? $resource->environment->project
|
||||
->environments()
|
||||
->with(['applications', 'services'])
|
||||
->with([
|
||||
'applications',
|
||||
'services',
|
||||
'postgresqls',
|
||||
'redis',
|
||||
'mongodbs',
|
||||
'mysqls',
|
||||
'mariadbs',
|
||||
'keydbs',
|
||||
'dragonflies',
|
||||
'clickhouses',
|
||||
])
|
||||
->get();
|
||||
$currentProjectUuid = data_get($resource, 'environment.project.uuid');
|
||||
$currentEnvironmentUuid = data_get($resource, 'environment.uuid');
|
||||
@@ -74,6 +90,16 @@
|
||||
class="relative w-48 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
|
||||
@foreach ($environments as $environment)
|
||||
@php
|
||||
// Use pre-loaded relations instead of databases() method to avoid N+1 queries
|
||||
$envDatabases = collect()
|
||||
->merge($environment->postgresqls ?? collect())
|
||||
->merge($environment->redis ?? collect())
|
||||
->merge($environment->mongodbs ?? collect())
|
||||
->merge($environment->mysqls ?? collect())
|
||||
->merge($environment->mariadbs ?? collect())
|
||||
->merge($environment->keydbs ?? collect())
|
||||
->merge($environment->dragonflies ?? collect())
|
||||
->merge($environment->clickhouses ?? collect());
|
||||
$envResources = collect()
|
||||
->merge(
|
||||
$environment->applications->map(
|
||||
@@ -81,9 +107,7 @@
|
||||
),
|
||||
)
|
||||
->merge(
|
||||
$environment
|
||||
->databases()
|
||||
->map(fn($db) => ['type' => 'database', 'resource' => $db]),
|
||||
$envDatabases->map(fn($db) => ['type' => 'database', 'resource' => $db]),
|
||||
)
|
||||
->merge(
|
||||
$environment->services->map(
|
||||
@@ -173,7 +197,9 @@
|
||||
]),
|
||||
};
|
||||
$isCurrentResource = $res->uuid === $currentResourceUuid;
|
||||
$resHasMultipleServers = $resType === 'application' && method_exists($res, 'additional_servers') && $res->additional_servers()->count() > 0;
|
||||
// Use loaded relation count if available, otherwise check additional_servers_count attribute
|
||||
$resHasMultipleServers = $resType === 'application' && method_exists($res, 'additional_servers') &&
|
||||
($res->relationLoaded('additional_servers') ? $res->additional_servers->count() > 0 : ($res->additional_servers_count ?? 0) > 0);
|
||||
$resServerName = $resHasMultipleServers ? null : data_get($res, 'destination.server.name');
|
||||
@endphp
|
||||
<div @mouseenter="openRes('{{ $environment->uuid }}-{{ $res->uuid }}'); resPositions['{{ $environment->uuid }}-{{ $res->uuid }}'] = $el.offsetTop - ($el.closest('.overflow-y-auto')?.scrollTop || 0)"
|
||||
@@ -405,7 +431,9 @@
|
||||
$isApplication = $resourceType === 'App\Models\Application';
|
||||
$isService = $resourceType === 'App\Models\Service';
|
||||
$isDatabase = str_contains($resourceType, 'Database') || str_contains($resourceType, 'Standalone');
|
||||
$hasMultipleServers = $isApplication && method_exists($resource, 'additional_servers') && $resource->additional_servers()->count() > 0;
|
||||
// Use loaded relation count if available, otherwise check additional_servers_count attribute
|
||||
$hasMultipleServers = $isApplication && method_exists($resource, 'additional_servers') &&
|
||||
($resource->relationLoaded('additional_servers') ? $resource->additional_servers->count() > 0 : ($resource->additional_servers_count ?? 0) > 0);
|
||||
$serverName = $hasMultipleServers ? null : data_get($resource, 'destination.server.name');
|
||||
$routeParams = [
|
||||
'project_uuid' => $currentProjectUuid,
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
href="{{ route('settings.oauth') }}">
|
||||
OAuth
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('settings.scheduled-jobs') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('settings.scheduled-jobs') }}">
|
||||
Scheduled Jobs
|
||||
</a>
|
||||
<div class="flex-1"></div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -699,15 +699,21 @@
|
||||
class="search-result-item w-full text-left block px-4 py-3 hover:bg-warning-50 dark:hover:bg-warning-900/20 transition-colors focus:outline-none focus:bg-warning-100 dark:focus:bg-warning-900/30 border-transparent hover:border-warning-500 focus:border-warning-500">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div
|
||||
class="flex-shrink-0 w-10 h-10 rounded-lg bg-warning-100 dark:bg-warning-900/40 flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 text-warning-600 dark:text-warning-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>
|
||||
@if (! empty($item['logo']))
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center overflow-hidden">
|
||||
<img src="{{ asset($item['logo']) }}" alt="{{ $item['name'] }}" class="w-7 h-7 object-contain">
|
||||
</div>
|
||||
@else
|
||||
<div
|
||||
class="flex-shrink-0 w-10 h-10 rounded-lg bg-warning-100 dark:bg-warning-900/40 flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 text-warning-600 dark:text-warning-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>
|
||||
@endif
|
||||
<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">
|
||||
@@ -808,15 +814,22 @@
|
||||
class="search-result-item w-full text-left block px-4 py-3 hover:bg-warning-50 dark:hover:bg-warning-900/20 transition-colors focus:outline-none focus:bg-warning-100 dark:focus:bg-warning-900/30 border-transparent hover:border-warning-500 focus:border-warning-500">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div
|
||||
class="flex-shrink-0 w-10 h-10 rounded-lg bg-warning-100 dark:bg-warning-900/40 flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 text-warning-600 dark:text-warning-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>
|
||||
<template x-if="item.logo">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center overflow-hidden">
|
||||
<img :src="'/' + item.logo" :alt="item.name" class="w-7 h-7 object-contain">
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!item.logo">
|
||||
<div
|
||||
class="flex-shrink-0 w-10 h-10 rounded-lg bg-warning-100 dark:bg-warning-900/40 flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 text-warning-600 dark:text-warning-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>
|
||||
</template>
|
||||
<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"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<nav wire:poll.10000ms="checkStatus" class="pb-6">
|
||||
<x-resources.breadcrumbs :resource="$application" :parameters="$parameters" :title="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
<div class="navbar-main">
|
||||
<nav class="flex shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10">
|
||||
<nav class="flex shrink-0 gap-4 items-center whitespace-nowrap scrollbar min-h-10">
|
||||
<a class="{{ request()->routeIs('project.application.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.application.configuration', $parameters) }}">
|
||||
Configuration
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="grid grid-cols-1 gap-4 xl:grid-cols-2 -mt-1">
|
||||
@foreach ($projects as $project)
|
||||
<div class="relative gap-2 cursor-pointer coolbox group">
|
||||
<a href="{{ $project->navigateTo() }}" class="absolute inset-0"></a>
|
||||
<a href="{{ $project->navigateTo() }}" {{ wireNavigate() }} class="absolute inset-0"></a>
|
||||
<div class="flex flex-1 mx-6">
|
||||
<div class="flex flex-col justify-center flex-1">
|
||||
<div class="box-title">{{ $project->name }}</div>
|
||||
|
||||
@@ -448,14 +448,56 @@
|
||||
<h2>Select a Postgresql type</h2>
|
||||
<div>If you need extra extensions, you can select Supabase PostgreSQL (or others), otherwise select
|
||||
PostgreSQL
|
||||
17 (default).</div>
|
||||
<div class="flex flex-col gap-6 pt-8">
|
||||
18 (default).</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 pt-8">
|
||||
<div class="gap-2 coolbox group flex relative"
|
||||
:class="{ 'cursor-pointer': !selecting, 'cursor-not-allowed opacity-50': selecting }"
|
||||
x-on:click="!selecting && (selecting = true, $wire.setPostgresqlType('postgres:18-alpine'))"
|
||||
:disabled="selecting">
|
||||
<div class="flex flex-col">
|
||||
<div class="box-title">PostgreSQL 18 (default)</div>
|
||||
<div class="box-description">
|
||||
PostgreSQL is a powerful, open-source object-relational database system (no extensions).
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://hub.docker.com/_/postgres/" target="_blank"
|
||||
@click.stop
|
||||
class="absolute top-2 right-2 p-1.5 rounded hover:bg-neutral-200 dark:hover:bg-coolgray-300 transition-colors"
|
||||
title="View documentation">
|
||||
<svg class="w-4 h-4 text-neutral-600 dark:text-neutral-400" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="gap-2 coolbox group flex relative"
|
||||
:class="{ 'cursor-pointer': !selecting, 'cursor-not-allowed opacity-50': selecting }"
|
||||
x-on:click="!selecting && (selecting = true, $wire.setPostgresqlType('postgres:17-alpine'))"
|
||||
:disabled="selecting">
|
||||
<div class="flex flex-col">
|
||||
<div class="box-title">PostgreSQL 17 (default)</div>
|
||||
<div class="box-title">PostgreSQL 17</div>
|
||||
<div class="box-description">
|
||||
PostgreSQL is a powerful, open-source object-relational database system (no extensions).
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://hub.docker.com/_/postgres/" target="_blank"
|
||||
@click.stop
|
||||
class="absolute top-2 right-2 p-1.5 rounded hover:bg-neutral-200 dark:hover:bg-coolgray-300 transition-colors"
|
||||
title="View documentation">
|
||||
<svg class="w-4 h-4 text-neutral-600 dark:text-neutral-400" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="gap-2 coolbox group flex relative"
|
||||
:class="{ 'cursor-pointer': !selecting, 'cursor-not-allowed opacity-50': selecting }"
|
||||
x-on:click="!selecting && (selecting = true, $wire.setPostgresqlType('postgres:16-alpine'))"
|
||||
:disabled="selecting">
|
||||
<div class="flex flex-col">
|
||||
<div class="box-title">PostgreSQL 16</div>
|
||||
<div class="box-description">
|
||||
PostgreSQL is a powerful, open-source object-relational database system (no extensions).
|
||||
</div>
|
||||
@@ -513,6 +555,27 @@
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="gap-2 coolbox group flex relative"
|
||||
:class="{ 'cursor-pointer': !selecting, 'cursor-not-allowed opacity-50': selecting }"
|
||||
x-on:click="!selecting && (selecting = true, $wire.setPostgresqlType('pgvector/pgvector:pg18'))"
|
||||
:disabled="selecting">
|
||||
<div class="flex flex-col">
|
||||
<div class="box-title">PGVector (18)</div>
|
||||
<div class="box-description">
|
||||
PGVector is a PostgreSQL extension for vector data types.
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://github.com/pgvector/pgvector" target="_blank"
|
||||
@click.stop
|
||||
class="absolute top-2 right-2 p-1.5 rounded hover:bg-neutral-200 dark:hover:bg-coolgray-300 transition-colors"
|
||||
title="View documentation">
|
||||
<svg class="w-4 h-4 text-neutral-600 dark:text-neutral-400" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="gap-2 coolbox group flex relative"
|
||||
:class="{ 'cursor-pointer': !selecting, 'cursor-not-allowed opacity-50': selecting }"
|
||||
x-on:click="!selecting && (selecting = true, $wire.setPostgresqlType('pgvector/pgvector:pg17'))"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{{ data_get_str($project, 'name')->limit(10) }} > Resources | Coolify
|
||||
</x-slot>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex min-w-0 flex-nowrap items-center gap-1">
|
||||
<h1>Resources</h1>
|
||||
@if ($environment->isEmpty())
|
||||
@can('createAnyResource')
|
||||
@@ -29,9 +29,6 @@
|
||||
<livewire:project.delete-environment :disabled="!$environment->isEmpty()" :environment_id="$environment->id" />
|
||||
@endcan
|
||||
</div>
|
||||
@php
|
||||
$projects = auth()->user()->currentTeam()->projects()->get();
|
||||
@endphp
|
||||
<nav class="flex pt-2 pb-6">
|
||||
<ol class="flex items-center">
|
||||
<li class="inline-flex items-center" x-data="{ projectOpen: false, toggle() { this.projectOpen = !this.projectOpen }, open() { this.projectOpen = true }, close() { this.projectOpen = false } }">
|
||||
@@ -53,7 +50,7 @@
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute z-20 top-full mt-1 w-56 -ml-2 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
|
||||
@foreach ($projects as $proj)
|
||||
@foreach ($allProjects as $proj)
|
||||
<a href="{{ route('project.show', ['project_uuid' => $proj->uuid]) }}"
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 {{ $proj->uuid === $project->uuid ? 'dark:text-warning font-semibold' : '' }}"
|
||||
title="{{ $proj->name }}">
|
||||
@@ -63,12 +60,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@php
|
||||
$allEnvironments = $project
|
||||
->environments()
|
||||
->with(['applications', 'services'])
|
||||
->get();
|
||||
@endphp
|
||||
<li class="inline-flex items-center" x-data="{ envOpen: false, activeEnv: null, envPositions: {}, activeRes: null, resPositions: {}, activeMenuEnv: null, menuPositions: {}, closeTimeout: null, envTimeout: null, resTimeout: null, menuTimeout: null, toggle() { this.envOpen = !this.envOpen; if (!this.envOpen) { this.activeEnv = null;
|
||||
this.activeRes = null;
|
||||
this.activeMenuEnv = null; } }, open() { clearTimeout(this.closeTimeout);
|
||||
@@ -162,6 +153,16 @@
|
||||
<!-- Resources Sub-dropdown (2nd level) -->
|
||||
@foreach ($allEnvironments as $env)
|
||||
@php
|
||||
// Use pre-loaded relations instead of databases() method to avoid N+1 queries
|
||||
$envDatabases = collect()
|
||||
->merge($env->postgresqls ?? collect())
|
||||
->merge($env->redis ?? collect())
|
||||
->merge($env->mongodbs ?? collect())
|
||||
->merge($env->mysqls ?? collect())
|
||||
->merge($env->mariadbs ?? collect())
|
||||
->merge($env->keydbs ?? collect())
|
||||
->merge($env->dragonflies ?? collect())
|
||||
->merge($env->clickhouses ?? collect());
|
||||
$envResources = collect()
|
||||
->merge(
|
||||
$env->applications->map(
|
||||
@@ -169,9 +170,7 @@
|
||||
),
|
||||
)
|
||||
->merge(
|
||||
$env
|
||||
->databases()
|
||||
->map(fn($db) => ['type' => 'database', 'resource' => $db]),
|
||||
$envDatabases->map(fn($db) => ['type' => 'database', 'resource' => $db]),
|
||||
)
|
||||
->merge(
|
||||
$env->services->map(fn($svc) => ['type' => 'service', 'resource' => $svc]),
|
||||
@@ -208,10 +207,11 @@
|
||||
'database_uuid' => $res->uuid,
|
||||
]),
|
||||
};
|
||||
// Use loaded relation to check additional_servers (avoids N+1 query)
|
||||
$resHasMultipleServers =
|
||||
$resType === 'application' &&
|
||||
method_exists($res, 'additional_servers') &&
|
||||
$res->additional_servers()->count() > 0;
|
||||
($res->relationLoaded('additional_servers') ? $res->additional_servers->count() > 0 : false);
|
||||
$resServerName = $resHasMultipleServers
|
||||
? null
|
||||
: data_get($res, 'destination.server.name');
|
||||
|
||||
@@ -71,8 +71,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" minWidth="32rem"
|
||||
maxWidth="40rem">
|
||||
<x-modal-input title="Edit Domains" :closeOutside="false">
|
||||
<x-slot:content>
|
||||
<span class="cursor-pointer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<h1>{{ $title }}</h1>
|
||||
<x-resources.breadcrumbs :resource="$service" :parameters="$parameters" />
|
||||
<div class="navbar-main" x-data">
|
||||
<nav class="flex shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10">
|
||||
<nav class="flex shrink-0 gap-4 items-center whitespace-nowrap scrollbar min-h-10">
|
||||
<a class="{{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.configuration', $parameters) }}">
|
||||
<button>Configuration</button>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<div class="subtitle">{{ data_get($server, 'name') }}</div>
|
||||
<div class="navbar-main">
|
||||
<nav
|
||||
class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar min-h-10 whitespace-nowrap pt-2">
|
||||
class="flex items-center gap-4 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'),
|
||||
]) }}" {{ wireNavigate() }}>
|
||||
@@ -197,4 +197,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -18,8 +18,10 @@
|
||||
<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."
|
||||
<x-forms.input canGate="update" :canResource="$settings" id="fqdn" label="URL"
|
||||
helper="Enter the full URL of the instance (for example, https://dashboard.example.com).<br><br>
|
||||
<span class='dark:text-warning text-coollabs'>Important: </span>
|
||||
If you want the dashboard to be accessible over HTTPS, you must include <b>https://</b> at the start of the URL. Without it, the dashboard will use HTTP and won’t be secured."
|
||||
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." />
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
<div>
|
||||
<x-slot:title>
|
||||
Scheduled Job Issues | Coolify
|
||||
</x-slot>
|
||||
<x-settings.navbar />
|
||||
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'executions' }"
|
||||
class="flex flex-col gap-8">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>Scheduled Job Issues</h2>
|
||||
<x-forms.button wire:click="refresh">Refresh</x-forms.button>
|
||||
</div>
|
||||
<div class="pb-4">Shows failed executions, skipped jobs, and scheduler health.</div>
|
||||
</div>
|
||||
|
||||
{{-- Tab Buttons --}}
|
||||
<div class="flex flex-row gap-4">
|
||||
<div @class([
|
||||
'box-without-bg cursor-pointer dark:bg-coolgray-100 dark:text-white w-full text-center items-center justify-center',
|
||||
])
|
||||
:class="activeTab === 'executions' && 'dark:bg-coollabs bg-coollabs text-white'"
|
||||
@click="activeTab = 'executions'; window.location.hash = 'executions'">
|
||||
Failures ({{ $executions->count() }})
|
||||
</div>
|
||||
<div @class([
|
||||
'box-without-bg cursor-pointer dark:bg-coolgray-100 dark:text-white w-full text-center items-center justify-center',
|
||||
])
|
||||
:class="activeTab === 'scheduler-runs' && 'dark:bg-coollabs bg-coollabs text-white'"
|
||||
@click="activeTab = 'scheduler-runs'; window.location.hash = 'scheduler-runs'">
|
||||
Scheduler Runs ({{ $managerRuns->count() }})
|
||||
</div>
|
||||
<div @class([
|
||||
'box-without-bg cursor-pointer dark:bg-coolgray-100 dark:text-white w-full text-center items-center justify-center',
|
||||
])
|
||||
:class="activeTab === 'skipped-jobs' && 'dark:bg-coollabs bg-coollabs text-white'"
|
||||
@click="activeTab = 'skipped-jobs'; window.location.hash = 'skipped-jobs'">
|
||||
Skipped Jobs ({{ $skipLogs->count() }})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Executions Tab --}}
|
||||
<div x-show="activeTab === 'executions'" x-cloak>
|
||||
{{-- Filters --}}
|
||||
<div class="flex gap-4 flex-wrap mb-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm font-medium">Type</label>
|
||||
<select wire:model.live="filterType"
|
||||
class="w-40 border bg-white dark:bg-coolgray-100 border-gray-300 dark:border-coolgray-400 rounded-md text-sm">
|
||||
<option value="all">All Types</option>
|
||||
<option value="backup">Backups</option>
|
||||
<option value="task">Tasks</option>
|
||||
<option value="cleanup">Docker Cleanup</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm font-medium">Time Range</label>
|
||||
<select wire:model.live="filterDate"
|
||||
class="w-40 border bg-white dark:bg-coolgray-100 border-gray-300 dark:border-coolgray-400 rounded-md text-sm">
|
||||
<option value="last_24h">Last 24 Hours</option>
|
||||
<option value="last_7d">Last 7 Days</option>
|
||||
<option value="last_30d">Last 30 Days</option>
|
||||
<option value="all">All Time</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-left">
|
||||
<thead class="text-xs uppercase bg-gray-50 dark:bg-coolgray-200">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Type</th>
|
||||
<th class="px-4 py-3">Resource</th>
|
||||
<th class="px-4 py-3">Server</th>
|
||||
<th class="px-4 py-3">Started</th>
|
||||
<th class="px-4 py-3">Duration</th>
|
||||
<th class="px-4 py-3">Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($executions as $execution)
|
||||
<tr wire:key="exec-{{ $execution['type'] }}-{{ $execution['id'] }}"
|
||||
class="border-b border-gray-200 dark:border-coolgray-400 hover:bg-gray-50 dark:hover:bg-coolgray-200">
|
||||
<td class="px-4 py-3">
|
||||
@php
|
||||
$typeLabel = match($execution['type']) {
|
||||
'backup' => 'Backup',
|
||||
'task' => 'Task',
|
||||
'cleanup' => 'Cleanup',
|
||||
default => ucfirst($execution['type']),
|
||||
};
|
||||
$typeBg = match($execution['type']) {
|
||||
'backup' => 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
|
||||
'task' => 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300',
|
||||
'cleanup' => 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300',
|
||||
default => 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300',
|
||||
};
|
||||
@endphp
|
||||
<span class="px-2 py-1 rounded-md text-xs font-medium {{ $typeBg }}">
|
||||
{{ $typeLabel }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
{{ $execution['resource_name'] }}
|
||||
@if($execution['resource_type'])
|
||||
<span class="text-xs text-gray-500">({{ $execution['resource_type'] }})</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3">{{ $execution['server_name'] }}</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
{{ $execution['created_at']->diffForHumans() }}
|
||||
<span class="block text-xs text-gray-500">{{ $execution['created_at']->format('M d H:i') }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
@if($execution['finished_at'] && $execution['created_at'])
|
||||
{{ \Carbon\Carbon::parse($execution['created_at'])->diffInSeconds(\Carbon\Carbon::parse($execution['finished_at'])) }}s
|
||||
@elseif($execution['status'] === 'running')
|
||||
<x-loading class="w-4 h-4" />
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 max-w-xs truncate" title="{{ $execution['message'] }}">
|
||||
{{ \Illuminate\Support\Str::limit($execution['message'], 80) }}
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="px-4 py-8 text-center text-gray-500">
|
||||
No failures found for the selected filters.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Scheduler Runs Tab --}}
|
||||
<div x-show="activeTab === 'scheduler-runs'" x-cloak>
|
||||
<div class="pb-4 text-sm text-gray-500">Shows when the ScheduledJobManager executed. Gaps indicate lock conflicts or missed runs.</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-left">
|
||||
<thead class="text-xs uppercase bg-gray-50 dark:bg-coolgray-200">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Time</th>
|
||||
<th class="px-4 py-3">Event</th>
|
||||
<th class="px-4 py-3">Duration</th>
|
||||
<th class="px-4 py-3">Dispatched</th>
|
||||
<th class="px-4 py-3">Skipped</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($managerRuns as $run)
|
||||
<tr wire:key="run-{{ $loop->index }}"
|
||||
class="border-b border-gray-200 dark:border-coolgray-400">
|
||||
<td class="px-4 py-2 whitespace-nowrap text-xs">{{ $run['timestamp'] }}</td>
|
||||
<td class="px-4 py-2">{{ $run['message'] }}</td>
|
||||
<td class="px-4 py-2">
|
||||
@if($run['duration_ms'] !== null)
|
||||
{{ $run['duration_ms'] }}ms
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-2">{{ $run['dispatched'] ?? '-' }}</td>
|
||||
<td class="px-4 py-2">
|
||||
@if(($run['skipped'] ?? 0) > 0)
|
||||
<span class="text-warning">{{ $run['skipped'] }}</span>
|
||||
@else
|
||||
{{ $run['skipped'] ?? '-' }}
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="px-4 py-4 text-center text-gray-500">
|
||||
No scheduler run logs found. Logs appear after the ScheduledJobManager runs.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Skipped Jobs Tab --}}
|
||||
<div x-show="activeTab === 'skipped-jobs'" x-cloak>
|
||||
<div class="pb-4 text-sm text-gray-500">Jobs that were not dispatched because conditions were not met.</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-left">
|
||||
<thead class="text-xs uppercase bg-gray-50 dark:bg-coolgray-200">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Time</th>
|
||||
<th class="px-4 py-3">Type</th>
|
||||
<th class="px-4 py-3">Reason</th>
|
||||
<th class="px-4 py-3">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($skipLogs as $skip)
|
||||
<tr wire:key="skip-{{ $loop->index }}"
|
||||
class="border-b border-gray-200 dark:border-coolgray-400">
|
||||
<td class="px-4 py-2 whitespace-nowrap text-xs">{{ $skip['timestamp'] }}</td>
|
||||
<td class="px-4 py-2">
|
||||
@php
|
||||
$skipTypeBg = match($skip['type']) {
|
||||
'backup' => 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
|
||||
'task' => 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300',
|
||||
'docker_cleanup' => 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300',
|
||||
default => 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300',
|
||||
};
|
||||
@endphp
|
||||
<span class="px-2 py-1 rounded-md text-xs font-medium {{ $skipTypeBg }}">
|
||||
{{ ucfirst(str_replace('_', ' ', $skip['type'])) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
@php
|
||||
$reasonLabel = match($skip['reason']) {
|
||||
'server_not_functional' => 'Server not functional',
|
||||
'subscription_unpaid' => 'Subscription unpaid',
|
||||
'database_deleted' => 'Database deleted',
|
||||
'server_deleted' => 'Server deleted',
|
||||
'resource_deleted' => 'Resource deleted',
|
||||
'application_not_running' => 'Application not running',
|
||||
'service_not_running' => 'Service not running',
|
||||
default => ucfirst(str_replace('_', ' ', $skip['reason'])),
|
||||
};
|
||||
$reasonBg = match($skip['reason']) {
|
||||
'server_not_functional', 'database_deleted', 'server_deleted', 'resource_deleted' => 'text-red-600 dark:text-red-400',
|
||||
'subscription_unpaid' => 'text-warning',
|
||||
'application_not_running', 'service_not_running' => 'text-orange-600 dark:text-orange-400',
|
||||
default => '',
|
||||
};
|
||||
@endphp
|
||||
<span class="{{ $reasonBg }}">{{ $reasonLabel }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-2 text-xs text-gray-500">
|
||||
@php
|
||||
$details = collect($skip['context'])
|
||||
->except(['type', 'skip_reason', 'execution_time'])
|
||||
->map(fn($v, $k) => str_replace('_', ' ', $k) . ': ' . $v)
|
||||
->implode(', ');
|
||||
@endphp
|
||||
{{ $details }}
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="px-4 py-4 text-center text-gray-500">
|
||||
No skipped jobs found. This means all scheduled jobs passed their conditions.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in new issue
Block a user