mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-19 18:15:31 +00:00
feat(ui): add shared table controls and multi-select filters
Introduce x-table toolbar/search/filter/sort/loading components and multi-select deployment history filters with OR within groups and AND across groups. Add live database/service status for breadcrumbs, shared variables layout controls, logs viewer toolbar polish, and infrastructure list filter consistency. Document patterns in UI_REDESIGN.md and cover with feature tests.
This commit is contained in:
1 parent
66b202adfe
commit
2a5197fb30
57 files changed
+1415
-579
No files matched your search
+47
-9
@@ -2489,6 +2489,14 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.logs-viewer-primary {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.logs-viewer-search {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
@@ -2612,7 +2620,7 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Status badge + cancel/force-start — right-aligned as a group */
|
||||
/* Cancel/force-start + search — right-aligned as a group */
|
||||
.logs-viewer-end {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
@@ -2623,6 +2631,15 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.logs-viewer-end .logs-viewer-meta {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.runtime-logs-viewer-end {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.logs-viewer-deployment-actions {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
@@ -2725,14 +2742,19 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
/* Desktop: [meta] [search] [actions] single row */
|
||||
/* Desktop: [actions + status] ... [cancel + search] */
|
||||
.logs-viewer-toolbar-controls {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.logs-viewer-primary {
|
||||
width: auto;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.logs-viewer-meta {
|
||||
order: 0;
|
||||
width: auto;
|
||||
@@ -2747,7 +2769,7 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
}
|
||||
|
||||
.logs-viewer-actions {
|
||||
order: 2;
|
||||
order: 0;
|
||||
width: auto;
|
||||
flex: 0 0 auto;
|
||||
max-width: 100%;
|
||||
@@ -2756,12 +2778,21 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
}
|
||||
|
||||
.logs-viewer-end {
|
||||
order: 3;
|
||||
order: 1;
|
||||
width: auto;
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.logs-viewer-end .logs-viewer-meta {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.runtime-logs-viewer-end {
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.logs-viewer-deployment-actions {
|
||||
width: auto;
|
||||
flex: 0 0 auto;
|
||||
@@ -2847,6 +2878,8 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
}
|
||||
|
||||
.runtime-log-toolbar {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
border-bottom-color: var(--coollabs-fill);
|
||||
background: transparent;
|
||||
}
|
||||
@@ -2886,13 +2919,18 @@ input[type="search"]::-webkit-search-results-decoration {
|
||||
}
|
||||
|
||||
.runtime-log-icon-button-active {
|
||||
background: color-mix(in oklab, var(--color-coollabs) 10%, transparent);
|
||||
color: var(--color-coollabs);
|
||||
background: var(--color-coollabs);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dark .runtime-log-icon-button-active {
|
||||
background: color-mix(in oklab, var(--color-warning) 14%, transparent);
|
||||
color: var(--color-warning);
|
||||
background: var(--color-coollabs);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.runtime-log-menu {
|
||||
background: var(--coollabs-elevated) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.runtime-log-viewport {
|
||||
|
||||
@@ -12,13 +12,6 @@
|
||||
|
||||
@php
|
||||
$items = match ($section) {
|
||||
'shared-variables' => [
|
||||
['label' => 'Overview', 'route' => 'shared-variables.index', 'active' => request()->routeIs('shared-variables.index')],
|
||||
['label' => 'Team', 'route' => 'shared-variables.team.index', 'active' => request()->routeIs('shared-variables.team.*')],
|
||||
['label' => 'Projects', 'route' => 'shared-variables.project.index', 'active' => request()->routeIs('shared-variables.project.*')],
|
||||
['label' => 'Environments', 'route' => 'shared-variables.environment.index', 'active' => request()->routeIs('shared-variables.environment.*')],
|
||||
['label' => 'Servers', 'route' => 'shared-variables.server.index', 'active' => request()->routeIs('shared-variables.server.*')],
|
||||
],
|
||||
'team' => [
|
||||
['label' => 'General', 'route' => 'team.index', 'active' => request()->routeIs('team.index', 'team.member.index', 'team.admin-view', 'team.danger-zone')],
|
||||
],
|
||||
@@ -79,14 +72,6 @@
|
||||
$hasTitle = filled($title);
|
||||
$hasActions = isset($actions);
|
||||
$showNav = $showTabs || $hasActions;
|
||||
$stackTabsOnMobile = $section === 'shared-variables';
|
||||
$sharedVariableIcons = [
|
||||
'Overview' => 'dashboard',
|
||||
'Team' => 'teams',
|
||||
'Projects' => 'projects',
|
||||
'Environments' => 'layers',
|
||||
'Servers' => 'servers',
|
||||
];
|
||||
@endphp
|
||||
|
||||
@if ($hasTitle)
|
||||
@@ -131,24 +116,7 @@
|
||||
<div
|
||||
class="flex w-full flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3 lg:h-full lg:gap-4">
|
||||
@if ($showTabs)
|
||||
@if ($stackTabsOnMobile)
|
||||
<div
|
||||
class="grid grid-cols-2 gap-0.5 border-y border-neutral-200 py-3 sm:grid-cols-3 lg:hidden dark:border-white/[0.06]">
|
||||
@foreach ($items as $item)
|
||||
<a @class([
|
||||
'menu-item',
|
||||
'menu-item-active' => $item['active'],
|
||||
]) {{ wireNavigate() }} href="{{ route($item['route'], $parameters) }}">
|
||||
<x-reicon :name="$sharedVariableIcons[$item['label']]" class="menu-item-icon" />
|
||||
<span class="menu-item-label">{{ $item['label'] }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
<div @class([
|
||||
'flex min-w-0 w-full items-center gap-0.5 overflow-x-auto rounded-[10px] border border-neutral-200 bg-neutral-100 p-1 sm:flex-1 dark:border-white/[0.07] dark:bg-white/[0.035]',
|
||||
'hidden lg:flex' => $stackTabsOnMobile,
|
||||
])>
|
||||
<div class="flex min-w-0 w-full items-center gap-0.5 overflow-x-auto rounded-[10px] border border-neutral-200 bg-neutral-100 p-1 sm:flex-1 dark:border-white/[0.07] dark:bg-white/[0.035]">
|
||||
@foreach ($items as $item)
|
||||
<a @class([
|
||||
'app-tab shrink-0',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@props(['server', 'activeMenu', 'activeSubMenu' => null])
|
||||
|
||||
@php
|
||||
$serverRouteParameters = ['server_uuid' => $server->uuid];
|
||||
$serverMenuItems = [
|
||||
@@ -49,14 +51,14 @@
|
||||
[
|
||||
'label' => 'Proxy',
|
||||
'route' => 'server.proxy',
|
||||
'active' => request()->routeIs('server.proxy', 'server.proxy.*'),
|
||||
'active' => $activeMenu === 'proxy',
|
||||
'icon' => 'network',
|
||||
'group' => 'Platform',
|
||||
'visible' => ! $server->isSwarmWorker() && ! $server->settings->is_build_server,
|
||||
'children' => [
|
||||
['label' => 'Configuration', 'route' => 'server.proxy', 'active' => request()->routeIs('server.proxy'), 'icon' => 'settings'],
|
||||
['label' => 'Dynamic Configurations', 'route' => 'server.proxy.dynamic-confs', 'active' => request()->routeIs('server.proxy.dynamic-confs'), 'icon' => 'sliders', 'visible' => $server->proxySet()],
|
||||
['label' => 'Logs', 'route' => 'server.proxy.logs', 'active' => request()->routeIs('server.proxy.logs'), 'icon' => 'file-content', 'visible' => $server->proxySet(), 'navigate' => false],
|
||||
['label' => 'Configuration', 'route' => 'server.proxy', 'active' => $activeSubMenu === 'configuration', 'icon' => 'settings'],
|
||||
['label' => 'Dynamic Configurations', 'route' => 'server.proxy.dynamic-confs', 'active' => $activeSubMenu === 'dynamic-confs', 'icon' => 'sliders', 'visible' => $server->proxySet()],
|
||||
['label' => 'Logs', 'route' => 'server.proxy.logs', 'active' => $activeSubMenu === 'logs', 'icon' => 'file-content', 'visible' => $server->proxySet(), 'navigate' => false],
|
||||
],
|
||||
],
|
||||
[
|
||||
|
||||
@@ -45,7 +45,8 @@
|
||||
<span>{{ $serverReady ? 'Ready' : 'Unavailable' }}</span>
|
||||
</div>
|
||||
@if ($server->proxySet())
|
||||
<div class="listbox-option cursor-default! gap-2.5!">
|
||||
<a href="{{ route('server.proxy', ['server_uuid' => $server->uuid]) }}" {{ wireNavigate() }}
|
||||
class="listbox-option gap-2.5!" @click="open = false" role="menuitem">
|
||||
<span @class([
|
||||
'size-1.5 shrink-0 rounded-full',
|
||||
'bg-success' => $proxyStatus === 'running',
|
||||
@@ -54,14 +55,15 @@
|
||||
])></span>
|
||||
<span class="flex-1">Proxy</span>
|
||||
<span>{{ str($proxyStatus ?: 'unknown')->headline() }}</span>
|
||||
</div>
|
||||
</a>
|
||||
@endif
|
||||
@if ($showSentinelStatus)
|
||||
<div class="listbox-option cursor-default! gap-2.5!">
|
||||
<a href="{{ route('server.sentinel', ['server_uuid' => $server->uuid]) }}" {{ wireNavigate() }}
|
||||
class="listbox-option gap-2.5!" @click="open = false" role="menuitem">
|
||||
<span class="size-1.5 shrink-0 rounded-full {{ $server->isSentinelLive() ? 'bg-success' : 'bg-error' }}"></span>
|
||||
<span class="flex-1">Sentinel</span>
|
||||
<span>{{ $server->isSentinelLive() ? 'In sync' : 'Out of sync' }}</span>
|
||||
</div>
|
||||
</a>
|
||||
@endif
|
||||
@if ($server->proxySet())
|
||||
<button type="button"
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
->values();
|
||||
@endphp
|
||||
|
||||
<x-shared-variables.layout>
|
||||
<div class="application-settings-form w-full" x-data="{
|
||||
sharedSearch: '',
|
||||
sharedSort: 'alphabetical',
|
||||
@@ -29,9 +30,6 @@
|
||||
return this.rows.filter(row => [row.key, row.comment, row.scope].some(value => value.includes(query))).length;
|
||||
}
|
||||
}">
|
||||
<x-dashboard.navbar section="shared-variables" title="Shared variables"
|
||||
subtitle="Reusable environment variables across resources" :titleOnDesktop="false" />
|
||||
|
||||
<x-application.settings-section :title="$title" flush>
|
||||
<x-slot:actions>
|
||||
<x-forms.button type="button" wire:click="switch" class="whitespace-nowrap">
|
||||
@@ -136,3 +134,4 @@
|
||||
@endif
|
||||
</x-application.settings-section>
|
||||
</div>
|
||||
</x-shared-variables.layout>
|
||||
@@ -0,0 +1,36 @@
|
||||
@php
|
||||
$sharedVariablesMenuItems = [
|
||||
['label' => 'Overview', 'route' => 'shared-variables.index', 'icon' => 'dashboard', 'active' => request()->routeIs('shared-variables.index')],
|
||||
['label' => 'Team', 'route' => 'shared-variables.team.index', 'icon' => 'teams', 'active' => request()->routeIs('shared-variables.team.*')],
|
||||
['label' => 'Projects', 'route' => 'shared-variables.project.index', 'icon' => 'projects', 'active' => request()->routeIs('shared-variables.project.*')],
|
||||
['label' => 'Environments', 'route' => 'shared-variables.environment.index', 'icon' => 'layers', 'active' => request()->routeIs('shared-variables.environment.*')],
|
||||
['label' => 'Servers', 'route' => 'shared-variables.server.index', 'icon' => 'servers', 'active' => request()->routeIs('shared-variables.server.*')],
|
||||
];
|
||||
@endphp
|
||||
|
||||
<section class="w-full max-w-[1180px]">
|
||||
<header class="mb-6 xl:hidden">
|
||||
<h1 class="text-[24px]! leading-7! font-semibold! tracking-tight!">Shared variables</h1>
|
||||
<p class="mt-1 text-[13px] text-neutral-500 dark:text-fg-dim">Reusable environment variables across resources</p>
|
||||
</header>
|
||||
|
||||
<div class="grid min-w-0 gap-8 xl:grid-cols-[210px_minmax(0,1fr)] xl:gap-10">
|
||||
<aside class="min-w-0 xl:self-start">
|
||||
<nav aria-label="Shared variables"
|
||||
class="grid grid-cols-2 gap-0.5 border-y border-neutral-200 py-3 sm:grid-cols-3 lg:grid-cols-5 xl:grid-cols-1 xl:border-y-0 xl:py-0 dark:border-white/[0.06]">
|
||||
@foreach ($sharedVariablesMenuItems as $menuItem)
|
||||
<a wire:key="shared-variables-{{ str($menuItem['label'])->slug() }}"
|
||||
@class(['menu-item', 'menu-item-active' => $menuItem['active']])
|
||||
{{ wireNavigate() }} href="{{ route($menuItem['route']) }}">
|
||||
<x-reicon :name="$menuItem['icon']" class="menu-item-icon" />
|
||||
<span class="menu-item-label">{{ $menuItem['label'] }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div class="min-w-0">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,31 @@
|
||||
@props(['label', 'storageKey'])
|
||||
|
||||
<div class="mb-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="relative w-full sm:max-w-sm">
|
||||
<x-reicon name="search"
|
||||
class="pointer-events-none absolute top-1/2 left-2.5 z-10 size-3.5 -translate-y-1/2 text-neutral-400 dark:text-fg-faint" />
|
||||
<input x-model.debounce.150ms="search" type="search" placeholder="Search {{ strtolower($label) }}"
|
||||
class="h-8! w-full rounded-lg! border-neutral-200! bg-white! py-0! pr-8! pl-8! text-[12px]! shadow-none! placeholder:text-neutral-400 focus:border-accent! focus:ring-0! dark:border-white/[0.08]! dark:bg-white/[0.035]! dark:text-fg! dark:placeholder:text-fg-faint">
|
||||
<button x-cloak x-show="search" @click="search = ''" type="button"
|
||||
class="absolute top-1/2 right-2 flex size-5 -translate-y-1/2 items-center justify-center rounded text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-faint dark:hover:bg-white/[0.07] dark:hover:text-fg"
|
||||
aria-label="Clear search">
|
||||
<x-reicon name="x" class="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex h-8 w-fit items-center rounded-lg border border-neutral-200 bg-white p-0.5 dark:border-white/[0.08] dark:bg-white/[0.035]">
|
||||
<button type="button" @click="viewMode = 'list'; localStorage.setItem('{{ $storageKey }}', 'list')"
|
||||
class="flex size-6.5 items-center justify-center rounded-md transition-colors"
|
||||
:class="viewMode === 'list' ? 'control-selected' : 'text-neutral-400 hover:bg-neutral-100 hover:text-black dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg'"
|
||||
aria-label="List view" title="List view">
|
||||
<x-reicon name="unordered-list" class="size-3.5" />
|
||||
</button>
|
||||
<button type="button" @click="viewMode = 'grid'; localStorage.setItem('{{ $storageKey }}', 'grid')"
|
||||
class="flex size-6.5 items-center justify-center rounded-md transition-colors"
|
||||
:class="viewMode === 'grid' ? 'control-selected' : 'text-neutral-400 hover:bg-neutral-100 hover:text-black dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg'"
|
||||
aria-label="Grid view" title="Grid view">
|
||||
<x-reicon name="grid" class="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
@props(['activeCount' => 0, 'activeText' => null, 'resetAction', 'resetLabel' => 'Reset filters'])
|
||||
|
||||
<div class="table-filter relative" x-data="{ open: false }" @keydown.escape.window="open = false">
|
||||
<button type="button" @click="open = !open" @click.outside="open = false" aria-haspopup="listbox"
|
||||
:aria-expanded="open" @if ($activeText) title="{{ $activeText }}" @endif
|
||||
@class(['button max-w-80 min-w-0', 'button-highlighted' => $activeCount > 0])>
|
||||
<x-reicon name="filter" class="size-3.5 shrink-0" />
|
||||
<span class="truncate">Filter</span>
|
||||
@if ($activeCount > 0)
|
||||
<span class="shrink-0 rounded-full bg-neutral-100 px-1.5 py-0.5 text-[10px] font-medium text-neutral-500 dark:bg-white/[0.07] dark:text-fg-dim">{{ $activeCount }}</span>
|
||||
@endif
|
||||
</button>
|
||||
<div class="listbox-panel left-auto! right-0! z-[90]! min-w-44! overflow-hidden! p-0!" x-show="open"
|
||||
x-cloak role="listbox" aria-multiselectable="true">
|
||||
<div class="max-h-80 overflow-y-auto p-1">{{ $slot }}</div>
|
||||
<div class="border-t border-neutral-200 bg-white p-1 dark:border-white/10 dark:bg-raised">
|
||||
<button type="button" class="listbox-option text-neutral-500 dark:text-fg-dim"
|
||||
wire:click="{{ $resetAction }}" @click="open = false" @disabled($activeCount === 0)>
|
||||
<span>{{ $resetLabel }}</span>
|
||||
<x-reicon name="x" class="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
@props(['target', 'text' => 'Loading records...'])
|
||||
|
||||
<div wire:loading.flex wire:target="{{ $target }}"
|
||||
{{ $attributes->class(['table-loading-overlay absolute inset-0 z-30 hidden items-center justify-center bg-white/70 backdrop-blur-[1px] dark:bg-black/20']) }}>
|
||||
<x-loading aria-label="{{ $text }}" class="[&_.loading-indicator]:size-5" />
|
||||
</div>
|
||||
@@ -0,0 +1,33 @@
|
||||
@props([
|
||||
'placeholder' => 'Search',
|
||||
'loadingTarget' => null,
|
||||
'disabled' => false,
|
||||
'clearAction' => null,
|
||||
'clearWhen' => null,
|
||||
])
|
||||
|
||||
<div class="table-search relative min-w-0 w-full">
|
||||
<input type="search" placeholder="{{ $placeholder }}" aria-label="{{ $placeholder }}"
|
||||
{{ $attributes->class(['input w-full pl-8!'])->except(['loading-target']) }} @disabled($disabled) />
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-2.5">
|
||||
@if ($loadingTarget)
|
||||
<x-reicon name="search" class="size-3.5 text-neutral-400 dark:text-fg-faint"
|
||||
wire:loading.remove wire:target="{{ $loadingTarget }}" />
|
||||
<svg wire:loading wire:target="{{ $loadingTarget }}" aria-hidden="true"
|
||||
class="size-3.5 animate-spin text-neutral-400 dark:text-fg-dim" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||
<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" />
|
||||
</svg>
|
||||
@else
|
||||
<x-reicon name="search" class="size-3.5 text-neutral-400 dark:text-fg-faint" />
|
||||
@endif
|
||||
</div>
|
||||
@if ($clearAction && $clearWhen)
|
||||
<button x-cloak x-show="{{ $clearWhen }}" x-on:click="{{ $clearAction }}" type="button"
|
||||
class="absolute top-1/2 right-2 flex size-5 -translate-y-1/2 items-center justify-center rounded text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-faint dark:hover:bg-white/[0.07] dark:hover:text-fg"
|
||||
aria-label="Clear search">
|
||||
<x-reicon name="x" class="size-3" />
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="table-sort relative" x-data="{ open: false }" @keydown.escape.window="open = false">
|
||||
<button type="button" class="button" @click="open = !open" @click.outside="open = false"
|
||||
aria-haspopup="listbox" :aria-expanded="open">
|
||||
<x-reicon name="sort-direction" class="size-3.5" />
|
||||
Sort
|
||||
</button>
|
||||
<div class="listbox-panel left-auto! right-0! z-[90]! min-w-44!" x-show="open" x-cloak role="listbox">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
<div {{ $attributes->class(['table-toolbar flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center']) }}>
|
||||
@isset($search)
|
||||
<div class="min-w-0 w-full flex-1 sm:max-w-md">{{ $search }}</div>
|
||||
@endisset
|
||||
<div class="flex flex-wrap items-center gap-2 sm:ml-auto">{{ $slot }}</div>
|
||||
</div>
|
||||
@@ -13,6 +13,14 @@
|
||||
$currentApplication = $currentEnvironment && $applicationUuid
|
||||
? $currentEnvironment->applications()->where('uuid', $applicationUuid)->first()
|
||||
: null;
|
||||
$databaseUuid = request()->route('database_uuid');
|
||||
$currentDatabase = $currentEnvironment && $databaseUuid
|
||||
? $currentEnvironment->databases()->firstWhere('uuid', $databaseUuid)
|
||||
: null;
|
||||
$serviceUuid = request()->route('service_uuid');
|
||||
$currentService = $currentEnvironment && $serviceUuid
|
||||
? $currentEnvironment->services()->where('uuid', $serviceUuid)->first()
|
||||
: null;
|
||||
$storageUuid = request()->route('storage_uuid');
|
||||
$storages = $storageUuid && $team ? \App\Models\S3Storage::ownedByCurrentTeam()->orderBy('name')->get() : collect();
|
||||
$currentStorage = $storages->firstWhere('uuid', $storageUuid);
|
||||
@@ -79,7 +87,7 @@
|
||||
])->filter()
|
||||
: collect();
|
||||
@endphp
|
||||
<div class="flex items-center gap-0.5 min-w-0 text-[13px]">
|
||||
<div class="flex min-w-0 items-center gap-0.5 text-[13px]">
|
||||
{{-- Team --}}
|
||||
<div class="shrink-0" x-data="{ collapsed: false }">
|
||||
<livewire:switch-team />
|
||||
@@ -251,4 +259,22 @@
|
||||
:wire:key="'application-status-'.$currentApplication->uuid" />
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@if ($currentDatabase)
|
||||
<span class="shrink-0 text-neutral-300 dark:text-fg-faint px-0.5">/</span>
|
||||
<span class="flex min-w-0 shrink items-center gap-2 h-8 px-2">
|
||||
<span class="min-w-0 truncate font-semibold text-black dark:text-fg">{{ $currentDatabase->name }}</span>
|
||||
<livewire:project.database.status :database="$currentDatabase"
|
||||
:wire:key="'database-status-'.$currentDatabase->uuid" />
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@if ($currentService)
|
||||
<span class="shrink-0 text-neutral-300 dark:text-fg-faint px-0.5">/</span>
|
||||
<span class="flex min-w-0 shrink items-center gap-2 h-8 px-2">
|
||||
<span class="min-w-0 truncate font-semibold text-black dark:text-fg">{{ $currentService->name }}</span>
|
||||
<livewire:project.service.status :service="$currentService"
|
||||
:wire:key="'service-status-'.$currentService->uuid" />
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
@@ -51,9 +51,10 @@
|
||||
</div>
|
||||
{{-- Collapse toggle + team switcher --}}
|
||||
<div class="flex items-center gap-0.5 min-w-0 flex-1 pl-3 pr-4">
|
||||
<x-top-breadcrumb />
|
||||
<div id="server-topbar-context" class="min-w-0"></div>
|
||||
<div class="flex-1"></div>
|
||||
<div class="relative flex min-w-0 flex-1 items-center">
|
||||
<x-top-breadcrumb />
|
||||
<div id="server-topbar-context" class="min-w-0"></div>
|
||||
</div>
|
||||
{{-- Dev Server-Timing HUD docks here (local only; empty in production) --}}
|
||||
<div id="server-timing-hud-slot" data-server-timing-hud-slot class="hidden shrink-0 items-center"></div>
|
||||
{{-- Resource actions dock here on desktop. --}}
|
||||
|
||||
@@ -33,9 +33,36 @@
|
||||
description="Add a Docker network endpoint to choose where your resources are deployed."
|
||||
icon-name="destinations" />
|
||||
@else
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@php
|
||||
$items = $destinations->map(fn ($destination) => [
|
||||
'name' => $destination->name,
|
||||
'server' => $destination->server->name,
|
||||
'type' => $destination->getMorphClass() === 'App\\Models\\SwarmDocker' ? 'Docker Swarm' : 'Standalone Docker',
|
||||
])->values();
|
||||
@endphp
|
||||
<div x-data="{
|
||||
search: '',
|
||||
viewMode: localStorage.getItem('coolify-destinations-view') || 'table',
|
||||
items: @js($items),
|
||||
get filteredItems() {
|
||||
const query = this.search.trim().toLowerCase();
|
||||
if (!query) return this.items;
|
||||
return this.items.filter(item => Object.values(item).some(value => String(value || '').toLowerCase().includes(query)));
|
||||
},
|
||||
matches(values) {
|
||||
const query = this.search.trim().toLowerCase();
|
||||
return !query || values.some(value => String(value || '').toLowerCase().includes(query));
|
||||
},
|
||||
setViewMode(mode) {
|
||||
this.viewMode = mode;
|
||||
localStorage.setItem('coolify-destinations-view', mode);
|
||||
}
|
||||
}">
|
||||
@include('livewire.shared.list-search-controls', ['placeholder' => 'Search destinations', 'singular' => 'destination', 'plural' => 'destinations'])
|
||||
|
||||
<div x-cloak x-show="viewMode === 'grid'" class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@foreach ($destinations as $destination)
|
||||
<a class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
<a x-show="matches(@js([$destination->name, $destination->server->name, $destination->getMorphClass() === 'App\\Models\\SwarmDocker' ? 'Docker Swarm' : 'Standalone Docker']))" class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
{{ wireNavigate() }}
|
||||
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}">
|
||||
<div class="flex items-start gap-3">
|
||||
@@ -63,5 +90,21 @@
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
<div x-show="viewMode === 'table'" class="overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
<div class="grid min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.7fr)_10rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
<div>Destination</div><div>Server</div><div>Type</div>
|
||||
</div>
|
||||
@foreach ($destinations as $destination)
|
||||
@php($isSwarm = $destination->getMorphClass() === 'App\\Models\\SwarmDocker')
|
||||
<a x-show="matches(@js([$destination->name, $destination->server->name, $isSwarm ? 'Docker Swarm' : 'Standalone Docker']))" {{ wireNavigate() }} href="{{ route('destination.show', ['destination_uuid' => $destination->uuid]) }}"
|
||||
class="grid min-h-14 min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.7fr)_10rem] items-center border-b border-neutral-200 px-4 py-2.5 text-[12px] transition-colors last:border-b-0 hover:bg-neutral-50 hover:no-underline dark:border-white/[0.07] dark:hover:bg-white/[0.025]">
|
||||
<div class="truncate font-semibold text-black dark:text-fg">{{ $destination->name }}</div>
|
||||
<div class="truncate text-neutral-500 dark:text-fg-dim">{{ $destination->server->name }}</div>
|
||||
<div><x-status-badge :label="$isSwarm ? 'Docker Swarm' : 'Standalone Docker'" :type="$isSwarm ? 'warning' : 'success'" /></div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@include('livewire.shared.list-search-empty', ['label' => 'destinations'])
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -9,7 +9,7 @@
|
||||
$lastPage = max(1, (int) ceil($deployments_count / $defaultTake));
|
||||
$firstVisibleRow = $deployments_count === 0 ? 0 : $skip + 1;
|
||||
$lastVisibleRow = min($skip + $deployments->count(), $deployments_count);
|
||||
$hasActiveFilter = $deploymentFilter !== 'all' || filled($pull_request_id);
|
||||
$hasActiveFilter = count($deploymentFilters) > 0 || filled($pull_request_id);
|
||||
$hasActiveQuery = trim($search) !== '' || $hasActiveFilter;
|
||||
@endphp
|
||||
|
||||
@@ -30,85 +30,90 @@
|
||||
@if (!$skip) wire:poll.5000ms="reloadDeployments" @endif>
|
||||
<x-application.settings-section title="Deployment history"
|
||||
helper="Search, filter, and open a deployment to inspect its build logs." flush>
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-2 border-b border-neutral-200 p-3 dark:border-white/[0.08]">
|
||||
<div class="relative min-w-0 max-w-md flex-1">
|
||||
<input type="search" placeholder="Search deployments" aria-label="Search deployments"
|
||||
wire:model.live.debounce.300ms="search" class="input w-full pl-8!" />
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-2.5">
|
||||
<x-reicon name="search" wire:loading.remove wire:target="search"
|
||||
class="size-3.5 text-neutral-400 dark:text-fg-faint" />
|
||||
<svg wire:loading wire:target="search" aria-hidden="true"
|
||||
class="size-3.5 animate-spin text-neutral-400 dark:text-fg-dim" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10"
|
||||
stroke="currentColor" stroke-width="4" />
|
||||
<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" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<div class="relative" x-data="{ open: false }" @keydown.escape.window="open = false">
|
||||
<button type="button" class="button" @click="open = !open"
|
||||
@click.outside="open = false" aria-haspopup="listbox" :aria-expanded="open">
|
||||
<x-reicon name="filter" class="size-3.5" />
|
||||
Filter
|
||||
</button>
|
||||
<div class="listbox-panel left-auto! right-0! z-[90]! w-52! min-w-52!" x-show="open"
|
||||
x-cloak role="listbox">
|
||||
<button type="button" class="listbox-option" role="option"
|
||||
aria-selected="{{ !$hasActiveFilter ? 'true' : 'false' }}"
|
||||
wire:click="clearFilter" @click="open = false">
|
||||
<span>All deployments</span>
|
||||
@if (!$hasActiveFilter)
|
||||
<svg class="size-3.5 shrink-0" viewBox="0 0 24 24" fill="none">
|
||||
<path d="m4.5 12.75 6 6 9-13.5" stroke="currentColor"
|
||||
stroke-width="2.5" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
@endif
|
||||
</button>
|
||||
|
||||
<x-table.toolbar class="border-b border-neutral-200 p-3 dark:border-white/[0.08]">
|
||||
<x-slot:search>
|
||||
<x-table.search placeholder="Search deployments" loading-target="search"
|
||||
wire:model.live.debounce.300ms="search" />
|
||||
</x-slot:search>
|
||||
<x-table.filter :active-count="count($deploymentFilters) + (filled($pull_request_id) ? 1 : 0)"
|
||||
reset-action="clearFilter">
|
||||
@if (count($statusFilterOptions) > 0)
|
||||
<span
|
||||
class="px-2 pb-1 pt-2 text-[10px] font-medium uppercase tracking-wider text-neutral-400 dark:text-fg-faint">Status</span>
|
||||
@foreach ($statusFilterOptions as $option)
|
||||
<button type="button" class="listbox-option" role="option"
|
||||
aria-selected="{{ $deploymentFilter === $option['value'] ? 'true' : 'false' }}"
|
||||
wire:click="setDeploymentFilter('{{ $option['value'] }}')"
|
||||
@click="open = false">
|
||||
aria-selected="{{ in_array($option['value'], $deploymentFilters, true) ? 'true' : 'false' }}"
|
||||
wire:click="toggleDeploymentFilter('{{ $option['value'] }}')">
|
||||
<span>{{ $option['label'] }}</span>
|
||||
@if ($deploymentFilter === $option['value'] && !$pull_request_id)
|
||||
<svg class="size-3.5 shrink-0" viewBox="0 0 24 24"
|
||||
fill="none">
|
||||
<path d="m4.5 12.75 6 6 9-13.5" stroke="currentColor"
|
||||
stroke-width="2.5" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
@endif
|
||||
@php
|
||||
$selected = in_array($option['value'], $deploymentFilters, true);
|
||||
@endphp
|
||||
<span @class([
|
||||
'flex size-4 shrink-0 items-center justify-center rounded-[5px] border',
|
||||
'border-coollabs bg-coollabs text-white dark:border-warning dark:bg-warning dark:text-black' => $selected,
|
||||
'border-neutral-300 bg-white dark:border-white/[0.14] dark:bg-white/[0.045]' => ! $selected,
|
||||
])>
|
||||
@if ($selected)
|
||||
<svg class="size-3" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<path d="m2.25 6.15 2.35 2.3 5.15-5" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
@endif
|
||||
</span>
|
||||
</button>
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
@if (count($sourceFilterOptions) > 1)
|
||||
@if (count($sourceFilterOptions) > 0)
|
||||
<span
|
||||
class="px-2 pb-1 pt-2 text-[10px] font-medium uppercase tracking-wider text-neutral-400 dark:text-fg-faint">Source</span>
|
||||
@foreach ($sourceFilterOptions as $option)
|
||||
<button type="button" class="listbox-option" role="option"
|
||||
aria-selected="{{ $deploymentFilter === $option['value'] ? 'true' : 'false' }}"
|
||||
wire:click="setDeploymentFilter('{{ $option['value'] }}')"
|
||||
@click="open = false">
|
||||
aria-selected="{{ in_array($option['value'], $deploymentFilters, true) ? 'true' : 'false' }}"
|
||||
wire:click="toggleDeploymentFilter('{{ $option['value'] }}')">
|
||||
<span>{{ $option['label'] }}</span>
|
||||
@if ($deploymentFilter === $option['value'] && !$pull_request_id)
|
||||
<svg class="size-3.5 shrink-0" viewBox="0 0 24 24"
|
||||
fill="none">
|
||||
<path d="m4.5 12.75 6 6 9-13.5" stroke="currentColor"
|
||||
stroke-width="2.5" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
@endif
|
||||
@php
|
||||
$selected = in_array($option['value'], $deploymentFilters, true);
|
||||
@endphp
|
||||
<span @class([
|
||||
'flex size-4 shrink-0 items-center justify-center rounded-[5px] border',
|
||||
'border-coollabs bg-coollabs text-white dark:border-warning dark:bg-warning dark:text-black' => $selected,
|
||||
'border-neutral-300 bg-white dark:border-white/[0.14] dark:bg-white/[0.045]' => ! $selected,
|
||||
])>
|
||||
@if ($selected)
|
||||
<svg class="size-3" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<path d="m2.25 6.15 2.35 2.3 5.15-5" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
@endif
|
||||
</span>
|
||||
</button>
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
@if (count($serverFilterOptions) > 0)
|
||||
<span
|
||||
class="px-2 pb-1 pt-2 text-[10px] font-medium uppercase tracking-wider text-neutral-400 dark:text-fg-faint">Server</span>
|
||||
@foreach ($serverFilterOptions as $option)
|
||||
<button type="button" class="listbox-option" role="option"
|
||||
aria-selected="{{ in_array($option['value'], $deploymentFilters, true) ? 'true' : 'false' }}"
|
||||
wire:click="toggleDeploymentFilter('{{ $option['value'] }}')">
|
||||
<span class="truncate">{{ $option['label'] }}</span>
|
||||
@php
|
||||
$selected = in_array($option['value'], $deploymentFilters, true);
|
||||
@endphp
|
||||
<span @class([
|
||||
'flex size-4 shrink-0 items-center justify-center rounded-[5px] border',
|
||||
'border-coollabs bg-coollabs text-white dark:border-warning dark:bg-warning dark:text-black' => $selected,
|
||||
'border-neutral-300 bg-white dark:border-white/[0.14] dark:bg-white/[0.045]' => ! $selected,
|
||||
])>
|
||||
@if ($selected)
|
||||
<svg class="size-3" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<path d="m2.25 6.15 2.35 2.3 5.15-5" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
@endif
|
||||
</span>
|
||||
</button>
|
||||
@endforeach
|
||||
@endif
|
||||
@@ -133,17 +138,8 @@
|
||||
</button>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative" x-data="{ open: false }" @keydown.escape.window="open = false">
|
||||
<button type="button" class="button" @click="open = !open"
|
||||
@click.outside="open = false" aria-haspopup="listbox" :aria-expanded="open">
|
||||
<x-reicon name="sort-direction" class="size-3.5" />
|
||||
Sort
|
||||
</button>
|
||||
<div class="listbox-panel left-auto! right-0! z-[90]! w-44! min-w-44!" x-show="open"
|
||||
x-cloak role="listbox">
|
||||
</x-table.filter>
|
||||
<x-table.sort>
|
||||
@foreach ([
|
||||
'newest' => 'Newest first',
|
||||
'oldest' => 'Oldest first',
|
||||
@@ -162,15 +158,16 @@
|
||||
@endif
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-table.sort>
|
||||
</x-table.toolbar>
|
||||
|
||||
@if ($deployments->isNotEmpty())
|
||||
<div class="data-table w-full transition-opacity"
|
||||
<div class="data-table relative w-full transition-opacity"
|
||||
wire:loading.class="opacity-50 pointer-events-none"
|
||||
wire:target="goToPage,previousPage,nextPage">
|
||||
wire:target="goToPage,previousPage,nextPage,toggleDeploymentFilter,clearFilter,setPullRequestFilter">
|
||||
<x-table.loading id="deployment-table-filter-loading"
|
||||
target="toggleDeploymentFilter,clearFilter,setPullRequestFilter"
|
||||
text="Filtering deployments..." class="rounded-lg" />
|
||||
<div class="data-table-header deployment-table-grid rounded-none!">
|
||||
<span>Status</span>
|
||||
<span>Source</span>
|
||||
|
||||
@@ -290,23 +290,55 @@
|
||||
};
|
||||
@endphp
|
||||
<div class="logs-viewer-toolbar-controls">
|
||||
<div class="logs-viewer-search relative">
|
||||
<x-reicon name="search"
|
||||
class="pointer-events-none absolute top-1/2 left-2.5 z-10 size-3.5 -translate-y-1/2 text-neutral-400 dark:text-neutral-500" />
|
||||
<input type="search" x-model.debounce.300ms="searchQuery" placeholder="Find in logs"
|
||||
aria-label="Find in logs"
|
||||
class="h-8! w-full rounded-lg! border-neutral-200! bg-white! py-0! pr-8! pl-8! text-[12px]! text-neutral-800! shadow-none! placeholder:text-neutral-400 focus:border-accent! focus:ring-0! dark:border-white/[0.08]! dark:bg-white/[0.05]! dark:text-white! dark:placeholder:text-neutral-500" />
|
||||
<button x-cloak x-show="searchQuery" x-on:click="searchQuery = ''" type="button"
|
||||
class="absolute top-1/2 right-2 z-10 flex size-5 -translate-y-1/2 items-center justify-center rounded text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-800 dark:text-neutral-500 dark:hover:bg-white/[0.07] dark:hover:text-white"
|
||||
aria-label="Clear search">
|
||||
<x-reicon name="x" class="size-3" />
|
||||
<div class="logs-viewer-primary">
|
||||
<div class="logs-viewer-actions">
|
||||
<button title="Toggle Timestamps" x-on:click="showTimestamps = !showTimestamps"
|
||||
:class="showTimestamps ? 'logs-viewer-btn-active' : ''"
|
||||
class="logs-viewer-btn">
|
||||
<svg class="size-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button title="Follow Logs" :class="alwaysScroll ? 'logs-viewer-btn-active' : ''"
|
||||
x-on:click="toggleScroll"
|
||||
class="logs-viewer-btn">
|
||||
<svg class="size-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2" d="M12 5v14m4-4l-4 4m-4-4l4 4" />
|
||||
</svg>
|
||||
</button>
|
||||
@can('update', $application)
|
||||
<button wire:click="toggleDebug"
|
||||
title="{{ $is_debug_enabled ? 'Hide Debug Logs' : 'Show Debug Logs' }}"
|
||||
class="logs-viewer-btn {{ $is_debug_enabled ? 'logs-viewer-btn-active' : '' }}">
|
||||
<svg class="size-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 12.75c1.148 0 2.278.08 3.383.237 1.037.146 1.866.966 1.866 2.013 0 3.728-2.35 6.75-5.25 6.75S6.75 18.728 6.75 15c0-1.046.83-1.867 1.866-2.013A24.204 24.204 0 0 1 12 12.75Zm0 0c2.883 0 5.647.508 8.207 1.44a23.91 23.91 0 0 1-1.152 6.06M12 12.75c-2.883 0-5.647.508-8.208 1.44.125 2.104.52 4.136 1.153 6.06M12 12.75a2.25 2.25 0 0 0 2.248-2.354M12 12.75a2.25 2.25 0 0 1-2.248-2.354M12 8.25c.995 0 1.971-.08 2.922-.236.403-.066.74-.358.795-.762a3.778 3.778 0 0 0-.399-2.25M12 8.25c-.995 0-1.97-.08-2.922-.236-.402-.066-.74-.358-.795-.762a3.734 3.734 0 0 1 .4-2.253M12 8.25a2.25 2.25 0 0 0-2.248 2.146M12 8.25a2.25 2.25 0 0 1 2.248 2.146M8.683 5a6.032 6.032 0 0 1-1.155-1.002c.07-.63.27-1.222.574-1.747m.581 2.749A3.75 3.75 0 0 1 15.318 5m0 0c.427-.283.815-.62 1.155-.999a4.471 4.471 0 0 0-.575-1.752M4.921 6a24.048 24.048 0 0 0-.392 3.314c1.668.546 3.416.914 5.223 1.082M19.08 6c.205 1.08.337 2.187.392 3.314a23.882 23.882 0 0 1-5.223 1.082" />
|
||||
</svg>
|
||||
</button>
|
||||
@endcan
|
||||
<button title="Fullscreen" x-show="!fullscreen" x-on:click="makeFullscreen"
|
||||
class="logs-viewer-btn">
|
||||
<svg class="size-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none">
|
||||
<path
|
||||
d="M24 0v24H0V0h24ZM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018Zm.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022Zm-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01l-.184-.092Z" />
|
||||
<path fill="currentColor"
|
||||
d="M9.793 12.793a1 1 0 0 1 1.497 1.32l-.083.094L6.414 19H9a1 1 0 0 1 .117 1.993L9 21H4a1 1 0 0 1-.993-.883L3 20v-5a1 1 0 0 1 1.993-.117L5 15v2.586l4.793-4.793ZM20 3a1 1 0 0 1 .993.883L21 4v5a1 1 0 0 1-1.993.117L19 9V6.414l-4.793 4.793a1 1 0 0 1-1.497-1.32l.083-.094L17.586 5H15a1 1 0 0 1-.117-1.993L15 3h5Z" />
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<button title="Minimize" x-show="fullscreen" x-on:click="makeFullscreen"
|
||||
class="logs-viewer-btn">
|
||||
<svg class="size-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 14h4m0 0v4m0-4l-6 6m14-10h-4m0 0V6m0 4l6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="logs-viewer-meta">
|
||||
<span x-show="searchQuery.trim()" x-text="matchCount + ' matches'"
|
||||
class="text-xs text-neutral-500 whitespace-nowrap"></span>
|
||||
</div>
|
||||
<div class="logs-viewer-actions">
|
||||
<button
|
||||
x-on:click="copyLogs()"
|
||||
title="Copy Logs"
|
||||
@@ -374,59 +406,29 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button title="Toggle Timestamps" x-on:click="showTimestamps = !showTimestamps"
|
||||
:class="showTimestamps ? 'logs-viewer-btn-active' : ''"
|
||||
class="logs-viewer-btn">
|
||||
<svg class="size-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
</button>
|
||||
@can('update', $application)
|
||||
<button wire:click="toggleDebug"
|
||||
title="{{ $is_debug_enabled ? 'Hide Debug Logs' : 'Show Debug Logs' }}"
|
||||
class="logs-viewer-btn {{ $is_debug_enabled ? 'logs-viewer-btn-active' : '' }}">
|
||||
<svg class="size-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 12.75c1.148 0 2.278.08 3.383.237 1.037.146 1.866.966 1.866 2.013 0 3.728-2.35 6.75-5.25 6.75S6.75 18.728 6.75 15c0-1.046.83-1.867 1.866-2.013A24.204 24.204 0 0 1 12 12.75Zm0 0c2.883 0 5.647.508 8.207 1.44a23.91 23.91 0 0 1-1.152 6.06M12 12.75c-2.883 0-5.647.508-8.208 1.44.125 2.104.52 4.136 1.153 6.06M12 12.75a2.25 2.25 0 0 0 2.248-2.354M12 12.75a2.25 2.25 0 0 1-2.248-2.354M12 8.25c.995 0 1.971-.08 2.922-.236.403-.066.74-.358.795-.762a3.778 3.778 0 0 0-.399-2.25M12 8.25c-.995 0-1.97-.08-2.922-.236-.402-.066-.74-.358-.795-.762a3.734 3.734 0 0 1 .4-2.253M12 8.25a2.25 2.25 0 0 0-2.248 2.146M12 8.25a2.25 2.25 0 0 1 2.248 2.146M8.683 5a6.032 6.032 0 0 1-1.155-1.002c.07-.63.27-1.222.574-1.747m.581 2.749A3.75 3.75 0 0 1 15.318 5m0 0c.427-.283.815-.62 1.155-.999a4.471 4.471 0 0 0-.575-1.752M4.921 6a24.048 24.048 0 0 0-.392 3.314c1.668.546 3.416.914 5.223 1.082M19.08 6c.205 1.08.337 2.187.392 3.314a23.882 23.882 0 0 1-5.223 1.082" />
|
||||
</svg>
|
||||
</button>
|
||||
@endcan
|
||||
<button title="Follow Logs" :class="alwaysScroll ? 'logs-viewer-btn-active' : ''"
|
||||
x-on:click="toggleScroll"
|
||||
class="logs-viewer-btn">
|
||||
<svg class="size-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2" d="M12 5v14m4-4l-4 4m-4-4l4 4" />
|
||||
</svg>
|
||||
</button>
|
||||
<button title="Fullscreen" x-show="!fullscreen" x-on:click="makeFullscreen"
|
||||
class="logs-viewer-btn">
|
||||
<svg class="size-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none">
|
||||
<path
|
||||
d="M24 0v24H0V0h24ZM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018Zm.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022Zm-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01l-.184-.092Z" />
|
||||
<path fill="currentColor"
|
||||
d="M9.793 12.793a1 1 0 0 1 1.497 1.32l-.083.094L6.414 19H9a1 1 0 0 1 .117 1.993L9 21H4a1 1 0 0 1-.993-.883L3 20v-5a1 1 0 0 1 1.993-.117L5 15v2.586l4.793-4.793ZM20 3a1 1 0 0 1 .993.883L21 4v5a1 1 0 0 1-1.993.117L19 9V6.414l-4.793 4.793a1 1 0 0 1-1.497-1.32l.083-.094L17.586 5H15a1 1 0 0 1-.117-1.993L15 3h5Z" />
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<button title="Minimize" x-show="fullscreen" x-on:click="makeFullscreen"
|
||||
class="logs-viewer-btn">
|
||||
<svg class="size-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 14h4m0 0v4m0-4l-6 6m14-10h-4m0 0V6m0 4l6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="logs-viewer-end">
|
||||
</div>
|
||||
<x-status-badge :status="$deploymentStatusLabel" :type="$deploymentStatusType"
|
||||
class="logs-viewer-status-badge" />
|
||||
</div>
|
||||
<div class="logs-viewer-end">
|
||||
<livewire:project.application.deployment-navbar
|
||||
:application_deployment_queue="$application_deployment_queue" />
|
||||
<div class="logs-viewer-search relative">
|
||||
<x-reicon name="search"
|
||||
class="pointer-events-none absolute top-1/2 left-2.5 z-10 size-3.5 -translate-y-1/2 text-neutral-400 dark:text-neutral-500" />
|
||||
<input type="search" x-model.debounce.300ms="searchQuery" placeholder="Find in logs"
|
||||
aria-label="Find in logs"
|
||||
class="h-8! w-full rounded-lg! border-neutral-200! bg-white! py-0! pr-8! pl-8! text-[12px]! text-neutral-800! shadow-none! placeholder:text-neutral-400 focus:border-accent! focus:ring-0! dark:border-white/[0.08]! dark:bg-white/[0.05]! dark:text-white! dark:placeholder:text-neutral-500" />
|
||||
<button x-cloak x-show="searchQuery" x-on:click="searchQuery = ''" type="button"
|
||||
class="absolute top-1/2 right-2 z-10 flex size-5 -translate-y-1/2 items-center justify-center rounded text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-800 dark:text-neutral-500 dark:hover:bg-white/[0.07] dark:hover:text-white"
|
||||
aria-label="Clear search">
|
||||
<x-reicon name="x" class="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="logs-viewer-meta">
|
||||
<span x-show="searchQuery.trim()" x-text="matchCount + ' matches'"
|
||||
class="text-xs text-neutral-500 whitespace-nowrap"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -64,18 +64,6 @@
|
||||
</x-slot:content>
|
||||
</x-process-dialog>
|
||||
|
||||
@teleport('#server-topbar-context')
|
||||
<div class="flex min-w-0 items-center gap-1 text-[13px]">
|
||||
<span class="shrink-0 px-0.5 text-neutral-300 dark:text-fg-faint">/</span>
|
||||
<span class="flex min-w-0 shrink items-center gap-2 px-1">
|
||||
<span class="max-w-48 min-w-0 truncate font-semibold text-black dark:text-fg xl:max-w-64">
|
||||
{{ $database->name }}
|
||||
</span>
|
||||
<x-status-badge :status="$databaseStatusLabel" :type="$databaseStatusType" />
|
||||
</span>
|
||||
</div>
|
||||
@endteleport
|
||||
|
||||
<div x-data>
|
||||
<div class="mb-3 w-full xl:hidden">
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
@php
|
||||
$databaseStatus = str($database->status ?? 'exited');
|
||||
[$statusDotClass, $statusLabel] = match (true) {
|
||||
$databaseStatus->startsWith('running') => ['bg-[#3fb950]', 'Running'],
|
||||
$databaseStatus->startsWith('degraded') => ['bg-orange-400', 'Degraded'],
|
||||
$databaseStatus->startsWith('restarting'),
|
||||
$databaseStatus->startsWith('starting') => ['bg-warning', 'Restarting'],
|
||||
default => ['bg-neutral-400 dark:bg-fg-faint', 'Stopped'],
|
||||
};
|
||||
@endphp
|
||||
|
||||
<span wire:poll.10000ms="refreshStatus"
|
||||
class="inline-flex h-[22px] shrink-0 items-center gap-1.5 rounded-full border border-neutral-200 bg-neutral-100 px-2.5 text-xs font-medium text-black dark:border-white/[0.12] dark:bg-white/[0.08] dark:text-fg"
|
||||
title="{{ $database->status }}">
|
||||
<span class="size-1.5 rounded-full {{ $statusDotClass }}"></span>
|
||||
{{ $statusLabel }}
|
||||
</span>
|
||||
@@ -156,7 +156,6 @@
|
||||
@php
|
||||
$app = collect($serviceApps)->firstWhere('id', (int) $appId);
|
||||
$heading = \Illuminate\Support\Str::headline($app['name'] ?? $rows->first()['service_name'] ?? 'Service');
|
||||
$appDomainCount = $rows->where('is_suggested', false)->count();
|
||||
$redirect = $serviceRedirects[$appId] ?? 'both';
|
||||
$redirectLabel = match ($redirect) {
|
||||
'www' => 'Redirect to www',
|
||||
@@ -168,9 +167,6 @@
|
||||
class="border-b border-neutral-200 last:border-b-0 dark:border-white/10">
|
||||
<div class="flex w-full items-center gap-3 px-4 py-3">
|
||||
<span class="min-w-0 flex-1 truncate text-sm font-medium text-black dark:text-white">{{ $heading }}</span>
|
||||
<span class="hidden shrink-0 text-xs text-neutral-500 sm:inline dark:text-fg-dim">
|
||||
{{ $appDomainCount }} domain{{ $appDomainCount === 1 ? '' : 's' }}
|
||||
</span>
|
||||
@can('update', $service)
|
||||
<div class="relative flex shrink-0 items-center gap-2 px-1 py-1 text-sm text-neutral-600 dark:text-fg-dim"
|
||||
wire:loading.class="opacity-50" wire:target="serviceRedirects.{{ $appId }}">
|
||||
|
||||
@@ -61,18 +61,6 @@
|
||||
</x-slot:content>
|
||||
</x-process-dialog>
|
||||
|
||||
@teleport('#server-topbar-context')
|
||||
<div class="flex min-w-0 items-center gap-1 text-[13px]">
|
||||
<span class="shrink-0 px-0.5 text-neutral-300 dark:text-fg-faint">/</span>
|
||||
<span class="flex min-w-0 shrink items-center gap-2 px-1">
|
||||
<span class="max-w-48 min-w-0 truncate font-semibold text-black dark:text-fg xl:max-w-64">
|
||||
{{ $service->name }}
|
||||
</span>
|
||||
<x-status-badge :status="$serviceStatusLabel" :type="$serviceStatusType" />
|
||||
</span>
|
||||
</div>
|
||||
@endteleport
|
||||
|
||||
<div x-data>
|
||||
<div class="mb-3 w-full xl:hidden">
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
@php
|
||||
$serviceStatus = str($service->status ?? 'exited');
|
||||
[$statusDotClass, $statusLabel] = match (true) {
|
||||
$serviceStatus->startsWith('running') => ['bg-[#3fb950]', 'Running'],
|
||||
$serviceStatus->startsWith('degraded') => ['bg-orange-400', 'Degraded'],
|
||||
$serviceStatus->startsWith('restarting'),
|
||||
$serviceStatus->startsWith('starting') => ['bg-warning', 'Restarting'],
|
||||
default => ['bg-neutral-400 dark:bg-fg-faint', 'Stopped'],
|
||||
};
|
||||
@endphp
|
||||
|
||||
<span wire:poll.10000ms="refreshStatus"
|
||||
class="inline-flex h-[22px] shrink-0 items-center gap-1.5 rounded-full border border-neutral-200 bg-neutral-100 px-2.5 text-xs font-medium text-black dark:border-white/[0.12] dark:bg-white/[0.08] dark:text-fg"
|
||||
title="{{ $service->status }}">
|
||||
<span class="size-1.5 rounded-full {{ $statusDotClass }}"></span>
|
||||
{{ $statusLabel }}
|
||||
</span>
|
||||
@@ -72,40 +72,14 @@
|
||||
|
||||
{{-- Toolbar: search left; filter and add right --}}
|
||||
@if ($view === 'normal')
|
||||
<div class="mt-2 flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center"
|
||||
@if (! $readyToLoad) aria-busy="true" @endif>
|
||||
<div class="relative min-w-0 w-full flex-1 sm:max-w-md">
|
||||
<input type="search" placeholder="Search environment variables"
|
||||
aria-label="Search environment variables" wire:model.live.debounce.300ms="search"
|
||||
class="input w-full pl-8!" @disabled(! $readyToLoad) />
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-2.5">
|
||||
<x-reicon name="search" wire:loading.remove wire:target="search,loadEnvironmentVariables"
|
||||
class="size-3.5 text-neutral-400 dark:text-fg-faint" />
|
||||
<svg wire:loading wire:target="search,loadEnvironmentVariables" aria-hidden="true"
|
||||
class="size-3.5 animate-spin text-neutral-400 dark:text-fg-dim" fill="none"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||
<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" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2 sm:ml-auto">
|
||||
<div class="relative" x-data="{ open: false }" @keydown.escape.window="open = false">
|
||||
<button type="button" @click="open = !open" @click.outside="open = false"
|
||||
@if ($activeFilterCount > 0) title="{{ $activeFilterText }}" @endif
|
||||
@class([
|
||||
'button max-w-80 min-w-0',
|
||||
'button-highlighted' => $activeFilterCount > 0,
|
||||
])>
|
||||
<x-reicon name="filter" class="size-3.5 shrink-0" />
|
||||
<span class="truncate">{{ $activeFilterCount > 0 ? $activeFilterText : 'Filter' }}</span>
|
||||
@if ($activeFilterCount > 0)
|
||||
<span class="shrink-0 rounded-full bg-neutral-100 px-1.5 py-0.5 text-[10px] font-medium text-neutral-500 dark:bg-white/[0.07] dark:text-fg-dim">{{ $activeFilterCount }}</span>
|
||||
@endif
|
||||
</button>
|
||||
<div class="listbox-panel left-auto! right-0! z-[90]! min-w-44! overflow-hidden! p-0!" x-show="open" x-cloak>
|
||||
<div class="max-h-80 overflow-y-auto p-1">
|
||||
<x-table.toolbar class="mt-2" aria-busy="{{ ! $readyToLoad ? 'true' : 'false' }}">
|
||||
<x-slot:search>
|
||||
<x-table.search placeholder="Search environment variables"
|
||||
loading-target="search,loadEnvironmentVariables" wire:model.live.debounce.300ms="search"
|
||||
:disabled="! $readyToLoad" />
|
||||
</x-slot:search>
|
||||
<x-table.filter :active-count="$activeFilterCount" :active-text="$activeFilterText"
|
||||
reset-action="clearFilters">
|
||||
@foreach ([
|
||||
'managed' => 'Managed',
|
||||
'user' => 'User-defined',
|
||||
@@ -174,31 +148,15 @@
|
||||
</button>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
<div class="relative z-20 border-t border-neutral-200 bg-white p-1 dark:border-white/10 dark:bg-[#171717]">
|
||||
<button type="button" class="listbox-option text-neutral-500 dark:text-fg-dim"
|
||||
wire:click="clearFilters" @click="open = false" @disabled($activeFilterCount === 0)>
|
||||
<span>Clear filters</span>
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path stroke-linecap="round" d="m6 6 12 12M18 6 6 18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative" x-data="{ open: false }" @keydown.escape.window="open = false">
|
||||
<button type="button" class="button" @click="open = !open" @click.outside="open = false">
|
||||
Sort
|
||||
</button>
|
||||
<div class="listbox-panel left-auto! right-0! z-[90]! min-w-44!" x-show="open" x-cloak>
|
||||
</x-table.filter>
|
||||
<x-table.sort>
|
||||
@foreach (['default' => 'Default order', 'name_asc' => 'Name A–Z', 'name_desc' => 'Name Z–A'] as $value => $label)
|
||||
<button type="button" class="listbox-option" wire:click="setTableSort('{{ $value }}')" @click="open = false">
|
||||
<span>{{ $label }}</span>
|
||||
@if ($tableSort === $value)<span>✓</span>@endif
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</x-table.sort>
|
||||
@can('manageEnvironment', $resource)
|
||||
{{-- Do not disable Add based on readyToLoad: modal-input uses wire:ignore, so a
|
||||
disabled attribute painted on first load would never re-enable. --}}
|
||||
@@ -213,8 +171,7 @@
|
||||
<livewire:project.shared.environment-variable.add />
|
||||
</x-modal-input>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
</x-table.toolbar>
|
||||
@endif
|
||||
|
||||
@if ($view === 'normal')
|
||||
@@ -266,11 +223,9 @@
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
<div wire:loading.flex
|
||||
wire:target="toggleVariableFilter,toggleServiceFilter,clearFilters,setEnvironmentFilter,setTableSort,setEnvironmentVariablePage,previousEnvironmentVariablePage,nextEnvironmentVariablePage"
|
||||
class="absolute inset-0 z-10 hidden items-center justify-center bg-black/5 backdrop-blur-[1px] dark:bg-black/20">
|
||||
<x-loading text="Loading environment variables..." />
|
||||
</div>
|
||||
<x-table.loading
|
||||
target="toggleVariableFilter,toggleServiceFilter,clearFilters,setEnvironmentFilter,setTableSort,setEnvironmentVariablePage,previousEnvironmentVariablePage,nextEnvironmentVariablePage"
|
||||
text="Loading environment variables..." />
|
||||
</div>
|
||||
<x-table-pagination :from="$firstVisibleRow" :to="$lastVisibleRow" :total="$totalRows"
|
||||
:current-page="$currentPage" :last-page="$lastPage"
|
||||
@@ -287,10 +242,7 @@
|
||||
description="Add your first variable with the + Add button above."
|
||||
icon-name="variables" />
|
||||
</div>
|
||||
<div wire:loading.flex wire:target="clearFilters"
|
||||
class="absolute inset-0 z-10 hidden items-center justify-center bg-black/5 backdrop-blur-[1px] dark:bg-black/20">
|
||||
<x-loading text="Loading environment variables..." />
|
||||
</div>
|
||||
<x-table.loading target="clearFilters" text="Loading environment variables..." />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -284,36 +284,14 @@
|
||||
:class="fullscreen ? 'h-full w-full' : ''">
|
||||
<div class="runtime-log-toolbar logs-viewer-toolbar">
|
||||
<div class="logs-viewer-toolbar-controls">
|
||||
<div class="logs-viewer-search relative">
|
||||
<x-reicon name="search"
|
||||
class="pointer-events-none absolute top-1/2 left-2.5 z-10 size-3.5 -translate-y-1/2 text-neutral-400 dark:text-fg-faint" />
|
||||
<input type="search" x-model.debounce.300ms="searchQuery" placeholder="Find in logs"
|
||||
aria-label="Find in logs"
|
||||
class="h-8! w-full rounded-lg! border-neutral-200! bg-white! py-0! pr-8! pl-8! text-[12px]! shadow-none! placeholder:text-neutral-400 focus:border-accent! focus:ring-0! dark:border-white/[0.08]! dark:bg-white/[0.035]! dark:text-fg! dark:placeholder:text-fg-faint" />
|
||||
<button x-cloak x-show="searchQuery" x-on:click="searchQuery = ''" type="button"
|
||||
class="absolute top-1/2 right-2 z-10 flex size-5 -translate-y-1/2 items-center justify-center rounded text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-faint dark:hover:bg-white/[0.07] dark:hover:text-fg"
|
||||
aria-label="Clear search">
|
||||
<x-reicon name="x" class="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="logs-viewer-meta">
|
||||
<form wire:submit="getLogs(true)" class="logs-viewer-lines">
|
||||
<span class="logs-viewer-lines-label">Lines</span>
|
||||
<input type="number" wire:model="numberOfLines" placeholder="100" min="1" max="50000"
|
||||
title="Number of Lines (max 50,000)" {{ $streamLogs ? 'readonly' : '' }}
|
||||
class="input logs-viewer-lines-input" />
|
||||
</form>
|
||||
<span x-show="searchQuery.trim()" x-text="matchCount + ' matches'"
|
||||
class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"></span>
|
||||
</div>
|
||||
<div class="logs-viewer-actions">
|
||||
<button wire:click="getLogs(true)" title="Refresh Logs" {{ $streamLogs ? 'disabled' : '' }}
|
||||
class="runtime-log-icon-button">
|
||||
class="runtime-log-icon-button order-8">
|
||||
<x-reicon name="refresh" class="size-3.5" />
|
||||
</button>
|
||||
<button wire:click="toggleStreamLogs"
|
||||
title="{{ $streamLogs ? 'Stop Streaming' : 'Stream Logs' }}"
|
||||
class="runtime-log-icon-button {{ $streamLogs ? 'runtime-log-icon-button-active' : '' }}">
|
||||
class="runtime-log-icon-button order-9 {{ $streamLogs ? 'runtime-log-icon-button-active' : '' }}">
|
||||
@if ($streamLogs)
|
||||
{{-- Pause icon --}}
|
||||
<svg class="size-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -345,14 +323,14 @@
|
||||
});
|
||||
"
|
||||
title="Copy Logs"
|
||||
class="runtime-log-icon-button">
|
||||
class="runtime-log-icon-button order-6">
|
||||
<svg class="size-4" 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="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
|
||||
</svg>
|
||||
</button>
|
||||
<div x-data="{ downloadMenuOpen: false, downloadingAllLogs: false }" class="relative shrink-0">
|
||||
<div x-data="{ downloadMenuOpen: false, downloadingAllLogs: false }" class="relative order-7 shrink-0">
|
||||
<button x-on:click="downloadMenuOpen = !downloadMenuOpen" title="Download Logs"
|
||||
class="runtime-log-icon-button">
|
||||
<svg class="size-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
@@ -368,7 +346,7 @@
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="transform opacity-100 scale-100"
|
||||
x-transition:leave-end="transform opacity-0 scale-95"
|
||||
class="listbox-panel left-auto! right-0! z-[90]! min-w-52!">
|
||||
class="runtime-log-menu listbox-panel left-auto! right-0! z-[90]! min-w-52!">
|
||||
<div>
|
||||
<button x-on:click="downloadLogs(); downloadMenuOpen = false"
|
||||
class="listbox-option">
|
||||
@@ -408,7 +386,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<button wire:click="toggleTimestamps" title="Toggle Timestamps"
|
||||
class="runtime-log-icon-button {{ $showTimeStamps ? 'runtime-log-icon-button-active' : '' }}">
|
||||
class="runtime-log-icon-button order-1 {{ $showTimeStamps ? 'runtime-log-icon-button-active' : '' }}">
|
||||
<svg class="size-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
@@ -417,14 +395,14 @@
|
||||
</button>
|
||||
<button title="Toggle Log Colors" x-on:click="toggleColorLogs"
|
||||
:class="colorLogs ? 'runtime-log-icon-button-active' : ''"
|
||||
class="runtime-log-icon-button">
|
||||
class="runtime-log-icon-button order-3">
|
||||
<svg class="size-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M9.53 16.122a3 3 0 0 0-5.78 1.128 2.25 2.25 0 0 1-2.4 2.245 4.5 4.5 0 0 0 8.4-2.245c0-.399-.078-.78-.22-1.128Zm0 0a15.998 15.998 0 0 0 3.388-1.62m-5.043-.025a15.994 15.994 0 0 1 1.622-3.395m3.42 3.42a15.995 15.995 0 0 0 4.764-4.648l3.876-5.814a1.151 1.151 0 0 0-1.597-1.597L14.146 6.32a15.996 15.996 0 0 0-4.649 4.763m3.42 3.42a6.776 6.776 0 0 0-3.42-3.42" />
|
||||
</svg>
|
||||
</button>
|
||||
<div x-data="{ filterOpen: false }" class="relative shrink-0">
|
||||
<div x-data="{ filterOpen: false }" class="relative order-4 shrink-0">
|
||||
<button x-on:click="filterOpen = !filterOpen" title="Filter Log Levels"
|
||||
:class="Object.values(logFilters).some(v => !v) ? 'runtime-log-icon-button-active' : ''"
|
||||
class="runtime-log-icon-button">
|
||||
@@ -441,7 +419,7 @@
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="transform opacity-100 scale-100"
|
||||
x-transition:leave-end="transform opacity-0 scale-95"
|
||||
class="listbox-panel left-auto! right-0! z-[90]! min-w-40!">
|
||||
class="runtime-log-menu listbox-panel left-auto! right-0! z-[90]! min-w-40!">
|
||||
<div>
|
||||
<button type="button" class="listbox-option" x-on:click="toggleLogFilter('error')">
|
||||
<span class="w-2.5 h-2.5 rounded-full bg-red-500"></span>
|
||||
@@ -468,14 +446,14 @@
|
||||
</div>
|
||||
<button title="Follow Logs" :class="alwaysScroll ? 'runtime-log-icon-button-active' : ''"
|
||||
x-on:click="toggleScroll"
|
||||
class="runtime-log-icon-button">
|
||||
class="runtime-log-icon-button order-2">
|
||||
<svg class="size-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M12 5v14m4-4l-4 4m-4-4l4 4" />
|
||||
</svg>
|
||||
</button>
|
||||
<button title="Fullscreen" x-show="!fullscreen" x-on:click="makeFullscreen"
|
||||
class="runtime-log-icon-button">
|
||||
class="runtime-log-icon-button order-5">
|
||||
<svg class="size-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none">
|
||||
<path
|
||||
@@ -486,13 +464,37 @@
|
||||
</svg>
|
||||
</button>
|
||||
<button title="Minimize" x-show="fullscreen" x-on:click="makeFullscreen"
|
||||
class="runtime-log-icon-button">
|
||||
class="runtime-log-icon-button order-5">
|
||||
<svg class="size-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M6 14h4m0 0v4m0-4l-6 6m14-10h-4m0 0V6m0 4l6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="logs-viewer-end runtime-logs-viewer-end">
|
||||
<div class="logs-viewer-meta">
|
||||
<form wire:submit="getLogs(true)" class="logs-viewer-lines">
|
||||
<span class="logs-viewer-lines-label">Lines</span>
|
||||
<input type="number" wire:model="numberOfLines" placeholder="100" min="1" max="50000"
|
||||
title="Number of Lines (max 50,000)" {{ $streamLogs ? 'readonly' : '' }}
|
||||
class="input logs-viewer-lines-input" />
|
||||
</form>
|
||||
<span x-show="searchQuery.trim()" x-text="matchCount + ' matches'"
|
||||
class="text-xs text-gray-500 whitespace-nowrap dark:text-gray-400"></span>
|
||||
</div>
|
||||
<div class="logs-viewer-search relative">
|
||||
<x-reicon name="search"
|
||||
class="pointer-events-none absolute top-1/2 left-2.5 z-10 size-3.5 -translate-y-1/2 text-neutral-400 dark:text-fg-faint" />
|
||||
<input type="search" x-model.debounce.300ms="searchQuery" placeholder="Find in logs"
|
||||
aria-label="Find in logs"
|
||||
class="h-8! w-full rounded-lg! border-neutral-200! bg-white! py-0! pr-8! pl-8! text-[12px]! shadow-none! placeholder:text-neutral-400 focus:border-accent! focus:ring-0! dark:border-white/[0.08]! dark:bg-white/[0.035]! dark:text-fg! dark:placeholder:text-fg-faint" />
|
||||
<button x-cloak x-show="searchQuery" x-on:click="searchQuery = ''" type="button"
|
||||
class="absolute top-1/2 right-2 z-10 flex size-5 -translate-y-1/2 items-center justify-center rounded text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-faint dark:hover:bg-white/[0.07] dark:hover:text-fg"
|
||||
aria-label="Clear search">
|
||||
<x-reicon name="x" class="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="logsContainer" @scroll="handleScroll" @wheel="handleWheel"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<div
|
||||
class="server-settings-workspace application-settings-workspace mt-4 grid w-full max-w-[1180px] min-w-0 gap-8 lg:mt-0 xl:grid-cols-[210px_minmax(0,1fr)] xl:gap-10">
|
||||
<x-server.sidebar :server="$server" activeMenu="proxy" />
|
||||
<x-server.sidebar :server="$server" activeMenu="proxy" activeSubMenu="dynamic-confs" />
|
||||
|
||||
<div class="application-settings-form flex w-full flex-col gap-6">
|
||||
@if ($server->isFunctional())
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<livewire:server.navbar :server="$server" />
|
||||
<div
|
||||
class="server-settings-workspace application-settings-workspace mt-4 grid w-full max-w-[1180px] min-w-0 gap-8 lg:mt-0 xl:grid-cols-[210px_minmax(0,1fr)] xl:gap-10">
|
||||
<x-server.sidebar :server="$server" activeMenu="proxy" />
|
||||
<x-server.sidebar :server="$server" activeMenu="proxy" activeSubMenu="logs" />
|
||||
<div class="application-settings-form w-full">
|
||||
<x-application.settings-section title="Proxy logs"
|
||||
helper="Search, filter, follow, copy, or download recent output from the Coolify proxy container."
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<livewire:server.navbar :server="$server" />
|
||||
<div
|
||||
class="server-settings-workspace application-settings-workspace mt-4 grid w-full max-w-[1180px] min-w-0 gap-8 lg:mt-0 xl:grid-cols-[210px_minmax(0,1fr)] xl:gap-10">
|
||||
<x-server.sidebar :server="$server" activeMenu="proxy" />
|
||||
<x-server.sidebar :server="$server" activeMenu="proxy" activeSubMenu="configuration" />
|
||||
@if ($server->isFunctional())
|
||||
<div class="w-full">
|
||||
<livewire:server.proxy :server="$server" />
|
||||
|
||||
@@ -1,63 +1,65 @@
|
||||
<div>
|
||||
<x-slot:title>
|
||||
Environment Variables | Coolify
|
||||
</x-slot>
|
||||
<x-slot:title>Environment Variables | Coolify</x-slot>
|
||||
|
||||
<x-dashboard.navbar section="shared-variables" title="Shared variables"
|
||||
subtitle="Environment-wide variables for resources in one environment" :titleOnDesktop="false" />
|
||||
<x-shared-variables.layout>
|
||||
<div class="w-full" x-data="{
|
||||
search: '',
|
||||
viewMode: localStorage.getItem('shared-variables-environments-view') || 'grid',
|
||||
matches(values) {
|
||||
const query = this.search.trim().toLowerCase();
|
||||
return !query || values.some(value => String(value || '').toLowerCase().includes(query));
|
||||
}
|
||||
}">
|
||||
@if ($projects->isEmpty())
|
||||
<x-empty title="No environments yet" description="Create a project environment before adding environment-wide variables." icon-name="layers" />
|
||||
@else
|
||||
<x-shared-variables.view-controls label="environments" storage-key="shared-variables-environments-view" />
|
||||
|
||||
<div class="w-full">
|
||||
@if ($projects->isEmpty())
|
||||
<x-empty title="No environments yet"
|
||||
description="Create a project environment before adding environment-wide variables."
|
||||
icon-name="layers" />
|
||||
@else
|
||||
<div class="flex flex-col gap-6">
|
||||
@foreach ($projects as $project)
|
||||
<section>
|
||||
<div class="mb-3">
|
||||
<h2 class="text-[14px]! leading-5! font-semibold! text-black dark:text-fg">
|
||||
{{ $project->name }}
|
||||
</h2>
|
||||
<p class="mt-0.5 text-[11px] text-neutral-500 dark:text-fg-faint">
|
||||
{{ $project->description ?: 'Project environments' }}
|
||||
</p>
|
||||
</div>
|
||||
<div x-cloak x-show="viewMode === 'grid'" class="flex flex-col gap-6">
|
||||
@foreach ($projects as $project)
|
||||
<section x-show="matches(@js([
|
||||
$project->name,
|
||||
$project->description,
|
||||
...$project->environments->flatMap(fn ($environment) => [$environment->name, $environment->description])->all(),
|
||||
]))">
|
||||
<div class="mb-3">
|
||||
<h2 class="text-[14px]! leading-5! font-semibold! text-black dark:text-fg">{{ $project->name }}</h2>
|
||||
<p class="mt-0.5 text-[11px] text-neutral-500 dark:text-fg-faint">{{ $project->description ?: 'Project environments' }}</p>
|
||||
</div>
|
||||
@if ($project->environments->isEmpty())
|
||||
<x-empty title="No environments in this project." size="sm" />
|
||||
@else
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@foreach ($project->environments as $environment)
|
||||
<a x-show="matches(@js([$environment->name, $environment->description, $project->name]))"
|
||||
class="group flex min-h-24 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
href="{{ route('shared-variables.environment.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid]) }}" {{ wireNavigate() }}>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-fg-dim"><x-reicon name="layers" class="size-4" /></div>
|
||||
<div class="min-w-0 flex-1"><h3 class="truncate text-[13px]! leading-4! font-semibold! text-black dark:text-fg">{{ $environment->name }}</h3><p class="mt-0.5 truncate text-[11px] text-neutral-500 dark:text-fg-faint">{{ $environment->description ?: 'No description' }}</p></div>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</section>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if ($project->environments->isEmpty())
|
||||
<x-empty title="No environments in this project." size="sm" />
|
||||
@else
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@foreach ($project->environments as $environment)
|
||||
<a class="group flex min-h-24 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
href="{{ route('shared-variables.environment.show', [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
]) }}"
|
||||
{{ wireNavigate() }}>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-fg-dim">
|
||||
<x-reicon name="layers" class="size-4" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h3
|
||||
class="truncate text-[13px]! leading-4! font-semibold! text-black dark:text-fg">
|
||||
{{ $environment->name }}
|
||||
</h3>
|
||||
<p
|
||||
class="mt-0.5 truncate text-[11px] text-neutral-500 dark:text-fg-faint">
|
||||
{{ $environment->description ?: 'No description' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</section>
|
||||
@endforeach
|
||||
<div x-show="viewMode === 'list'" class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
@foreach ($projects as $project)
|
||||
@foreach ($project->environments as $environment)
|
||||
<a x-show="matches(@js([$environment->name, $environment->description, $project->name]))"
|
||||
href="{{ route('shared-variables.environment.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid]) }}" {{ wireNavigate() }}
|
||||
class="flex min-h-14 items-center gap-3 border-b border-neutral-200 px-4 py-2.5 last:border-b-0 hover:bg-neutral-50 hover:no-underline dark:border-white/[0.07] dark:hover:bg-white/[0.025]">
|
||||
<x-reicon name="layers" class="size-4 shrink-0 text-neutral-500 dark:text-fg-dim" />
|
||||
<div class="min-w-0 flex-1"><div class="truncate text-[13px] font-medium">{{ $environment->name }}</div><div class="truncate text-[11px] text-neutral-500 dark:text-fg-faint">{{ $environment->description ?: 'No description' }}</div></div>
|
||||
<span class="shrink-0 text-[11px] text-neutral-500 dark:text-fg-dim">{{ $project->name }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-shared-variables.layout>
|
||||
</div>
|
||||
@@ -3,8 +3,7 @@
|
||||
Shared Variables | Coolify
|
||||
</x-slot>
|
||||
|
||||
<x-dashboard.navbar section="shared-variables" title="Shared variables"
|
||||
subtitle="Reusable environment variables across resources" :titleOnDesktop="false" />
|
||||
<x-shared-variables.layout>
|
||||
|
||||
<div class="w-full">
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@@ -65,4 +64,5 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</x-shared-variables.layout>
|
||||
</div>
|
||||
@@ -1,45 +1,54 @@
|
||||
<div>
|
||||
<x-slot:title>
|
||||
Project Variables | Coolify
|
||||
</x-slot>
|
||||
<x-slot:title>Project Variables | Coolify</x-slot>
|
||||
|
||||
<x-dashboard.navbar section="shared-variables" title="Shared variables"
|
||||
subtitle="Project-wide variables for every environment in a project" :titleOnDesktop="false" />
|
||||
<x-shared-variables.layout>
|
||||
<div class="w-full" x-data="{
|
||||
search: '',
|
||||
viewMode: localStorage.getItem('shared-variables-projects-view') || 'grid',
|
||||
matches(values) {
|
||||
const query = this.search.trim().toLowerCase();
|
||||
return !query || values.some(value => String(value || '').toLowerCase().includes(query));
|
||||
}
|
||||
}">
|
||||
@if ($projects->isEmpty())
|
||||
<x-empty title="No projects yet" description="Create a project before adding project-wide variables."
|
||||
icon-name="projects" />
|
||||
@else
|
||||
<x-shared-variables.view-controls label="projects" storage-key="shared-variables-projects-view" />
|
||||
|
||||
<div class="w-full">
|
||||
@if ($projects->isEmpty())
|
||||
<x-empty title="No projects yet"
|
||||
description="Create a project before adding project-wide variables."
|
||||
icon-name="projects" />
|
||||
@else
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@foreach ($projects as $project)
|
||||
<a class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
href="{{ route('shared-variables.project.show', ['project_uuid' => $project->uuid]) }}"
|
||||
{{ wireNavigate() }}>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-fg-dim">
|
||||
<x-reicon name="projects" class="size-4" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h2 class="truncate text-[13px]! leading-4! font-semibold! text-black dark:text-fg">
|
||||
{{ $project->name }}
|
||||
</h2>
|
||||
<p class="mt-0.5 truncate text-[11px] text-neutral-500 dark:text-fg-faint">
|
||||
{{ $project->description ?: 'No description' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-auto flex items-center pt-4">
|
||||
<span class="text-[11px] text-neutral-500 dark:text-fg-dim">
|
||||
{{ $project->environment_variables()->count() }}
|
||||
{{ Str::plural('variable', $project->environment_variables()->count()) }}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
<div x-cloak x-show="viewMode === 'grid'" class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@foreach ($projects as $project)
|
||||
<a x-show="matches(@js([$project->name, $project->description]))"
|
||||
class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
href="{{ route('shared-variables.project.show', ['project_uuid' => $project->uuid]) }}" {{ wireNavigate() }}>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-fg-dim">
|
||||
<x-reicon name="projects" class="size-4" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h2 class="truncate text-[13px]! leading-4! font-semibold! text-black dark:text-fg">{{ $project->name }}</h2>
|
||||
<p class="mt-0.5 truncate text-[11px] text-neutral-500 dark:text-fg-faint">{{ $project->description ?: 'No description' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-auto pt-4 text-[11px] text-neutral-500 dark:text-fg-dim">
|
||||
{{ $project->environment_variables()->count() }} {{ Str::plural('variable', $project->environment_variables()->count()) }}
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div x-show="viewMode === 'list'" class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
@foreach ($projects as $project)
|
||||
<a x-show="matches(@js([$project->name, $project->description]))"
|
||||
href="{{ route('shared-variables.project.show', ['project_uuid' => $project->uuid]) }}" {{ wireNavigate() }}
|
||||
class="flex min-h-14 items-center gap-3 border-b border-neutral-200 px-4 py-2.5 last:border-b-0 hover:bg-neutral-50 hover:no-underline dark:border-white/[0.07] dark:hover:bg-white/[0.025]">
|
||||
<x-reicon name="projects" class="size-4 shrink-0 text-neutral-500 dark:text-fg-dim" />
|
||||
<div class="min-w-0 flex-1"><div class="truncate text-[13px] font-medium">{{ $project->name }}</div><div class="truncate text-[11px] text-neutral-500 dark:text-fg-faint">{{ $project->description ?: 'No description' }}</div></div>
|
||||
<span class="shrink-0 text-[11px] text-neutral-500 dark:text-fg-dim">{{ $project->environment_variables()->count() }} {{ Str::plural('variable', $project->environment_variables()->count()) }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-shared-variables.layout>
|
||||
</div>
|
||||
@@ -1,43 +1,46 @@
|
||||
<div>
|
||||
<x-slot:title>
|
||||
Server Variables | Coolify
|
||||
</x-slot>
|
||||
<x-slot:title>Server Variables | Coolify</x-slot>
|
||||
|
||||
<x-dashboard.navbar section="shared-variables" title="Shared variables"
|
||||
subtitle="Server-wide variables available to resources on a server" :titleOnDesktop="false" />
|
||||
<x-shared-variables.layout>
|
||||
<div class="w-full" x-data="{
|
||||
search: '',
|
||||
viewMode: localStorage.getItem('shared-variables-servers-view') || 'grid',
|
||||
matches(values) {
|
||||
const query = this.search.trim().toLowerCase();
|
||||
return !query || values.some(value => String(value || '').toLowerCase().includes(query));
|
||||
}
|
||||
}">
|
||||
@if ($servers->isEmpty())
|
||||
<x-empty title="No servers yet" description="Add a server before creating server-wide variables." icon-name="servers" />
|
||||
@else
|
||||
<x-shared-variables.view-controls label="servers" storage-key="shared-variables-servers-view" />
|
||||
|
||||
<div class="w-full">
|
||||
@if ($servers->isEmpty())
|
||||
<x-empty title="No servers yet"
|
||||
description="Add a server before creating server-wide variables."
|
||||
icon-name="servers" />
|
||||
@else
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@foreach ($servers as $server)
|
||||
<a class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
href="{{ route('shared-variables.server.show', ['server_uuid' => $server->uuid]) }}"
|
||||
{{ wireNavigate() }}>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-fg-dim">
|
||||
<x-reicon name="servers" class="size-4" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h2 class="truncate text-[13px]! leading-4! font-semibold! text-black dark:text-fg">
|
||||
{{ $server->name }}
|
||||
</h2>
|
||||
<p class="mt-0.5 truncate text-[11px] text-neutral-500 dark:text-fg-faint">
|
||||
{{ $server->description ?: $server->ip }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-auto flex items-center pt-4">
|
||||
<x-status-badge :status="$server->isFunctional() ? 'Ready' : 'Validation required'"
|
||||
:type="$server->isFunctional() ? 'success' : 'warning'" />
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
<div x-cloak x-show="viewMode === 'grid'" class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@foreach ($servers as $server)
|
||||
<a x-show="matches(@js([$server->name, $server->description, $server->ip]))"
|
||||
class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
href="{{ route('shared-variables.server.show', ['server_uuid' => $server->uuid]) }}" {{ wireNavigate() }}>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-fg-dim"><x-reicon name="servers" class="size-4" /></div>
|
||||
<div class="min-w-0 flex-1"><h2 class="truncate text-[13px]! leading-4! font-semibold! text-black dark:text-fg">{{ $server->name }}</h2><p class="mt-0.5 truncate text-[11px] text-neutral-500 dark:text-fg-faint">{{ $server->description ?: $server->ip }}</p></div>
|
||||
</div>
|
||||
<div class="mt-auto pt-4"><x-status-badge :status="$server->isFunctional() ? 'Ready' : 'Validation required'" :type="$server->isFunctional() ? 'success' : 'warning'" /></div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div x-show="viewMode === 'list'" class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
@foreach ($servers as $server)
|
||||
<a x-show="matches(@js([$server->name, $server->description, $server->ip]))"
|
||||
href="{{ route('shared-variables.server.show', ['server_uuid' => $server->uuid]) }}" {{ wireNavigate() }}
|
||||
class="flex min-h-14 items-center gap-3 border-b border-neutral-200 px-4 py-2.5 last:border-b-0 hover:bg-neutral-50 hover:no-underline dark:border-white/[0.07] dark:hover:bg-white/[0.025]">
|
||||
<x-reicon name="servers" class="size-4 shrink-0 text-neutral-500 dark:text-fg-dim" />
|
||||
<div class="min-w-0 flex-1"><div class="truncate text-[13px] font-medium">{{ $server->name }}</div><div class="truncate text-[11px] text-neutral-500 dark:text-fg-faint">{{ $server->description ?: $server->ip }}</div></div>
|
||||
<x-status-badge :status="$server->isFunctional() ? 'Ready' : 'Validation required'" :type="$server->isFunctional() ? 'success' : 'warning'" />
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-shared-variables.layout>
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="w-full sm:max-w-sm">
|
||||
<x-table.search :placeholder="$placeholder" x-model.debounce.150ms="search"
|
||||
clear-when="search" clear-action="search = ''"
|
||||
class="h-8! rounded-lg! border-neutral-200! bg-white! py-0! pr-8! text-[12px]! shadow-none! placeholder:text-neutral-400 focus:border-accent! focus:ring-0! dark:border-white/[0.08]! dark:bg-white/[0.035]! dark:text-fg! dark:placeholder:text-fg-faint" />
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-[11px] text-neutral-500 dark:text-fg-faint"><span x-text="filteredItems.length"></span> <span x-text="filteredItems.length === 1 ? '{{ $singular }}' : '{{ $plural }}'"></span></span>
|
||||
<div class="flex h-8 items-center rounded-lg border border-neutral-200 bg-white p-0.5 dark:border-white/[0.08] dark:bg-white/[0.035]">
|
||||
<button type="button" x-on:click="setViewMode('table')" class="flex size-6.5 items-center justify-center rounded-md transition-colors" :class="viewMode === 'table' ? 'control-selected' : 'text-neutral-400 hover:bg-neutral-100 hover:text-black dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg'" aria-label="Table view"><x-reicon name="unordered-list" class="size-3.5" /></button>
|
||||
<button type="button" x-on:click="setViewMode('grid')" class="flex size-6.5 items-center justify-center rounded-md transition-colors" :class="viewMode === 'grid' ? 'control-selected' : 'text-neutral-400 hover:bg-neutral-100 hover:text-black dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg'" aria-label="Grid view"><x-reicon name="grid" class="size-3.5" /></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
<div x-show="filteredItems.length === 0" class="flex min-h-52 flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white px-6 text-center dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
<x-reicon name="search" class="mb-3 size-6 text-neutral-300 dark:text-fg-faint" />
|
||||
<p class="text-[13px] font-medium">No matching {{ $label }}</p>
|
||||
<p class="mt-1 text-[12px] text-neutral-500 dark:text-fg-dim">Try a different search.</p>
|
||||
</div>
|
||||
@@ -31,9 +31,23 @@
|
||||
description="Add an S3-compatible destination to store backups outside your servers."
|
||||
icon-name="storages" />
|
||||
@else
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@php
|
||||
$items = $s3->map(fn ($storage) => [
|
||||
'name' => $storage->name,
|
||||
'description' => $storage->description ?: 'S3-compatible storage',
|
||||
'status' => $storage->is_usable ? 'Connected' : 'Not usable',
|
||||
])->values();
|
||||
@endphp
|
||||
<div x-data="{
|
||||
search: '', viewMode: localStorage.getItem('coolify-s3-storages-view') || 'table', items: @js($items),
|
||||
get filteredItems() { const query = this.search.trim().toLowerCase(); return query ? this.items.filter(item => Object.values(item).some(value => String(value || '').toLowerCase().includes(query))) : this.items; },
|
||||
matches(values) { const query = this.search.trim().toLowerCase(); return !query || values.some(value => String(value || '').toLowerCase().includes(query)); },
|
||||
setViewMode(mode) { this.viewMode = mode; localStorage.setItem('coolify-s3-storages-view', mode); }
|
||||
}">
|
||||
@include('livewire.shared.list-search-controls', ['placeholder' => 'Search S3 storages', 'singular' => 'storage', 'plural' => 'storages'])
|
||||
<div x-cloak x-show="viewMode === 'grid'" class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@foreach ($s3 as $storage)
|
||||
<a {{ wireNavigate() }} href="/storages/{{ $storage->uuid }}"
|
||||
<a x-show="matches(@js([$storage->name, $storage->description ?: 'S3-compatible storage', $storage->is_usable ? 'Connected' : 'Not usable']))" {{ wireNavigate() }} href="/storages/{{ $storage->uuid }}"
|
||||
class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
@@ -60,5 +74,17 @@
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
<div x-show="viewMode === 'table'" class="overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
<div class="grid min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.8fr)_9rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint"><div>Storage</div><div>Description</div><div>Status</div></div>
|
||||
@foreach ($s3 as $storage)
|
||||
<a x-show="matches(@js([$storage->name, $storage->description ?: 'S3-compatible storage', $storage->is_usable ? 'Connected' : 'Not usable']))" {{ wireNavigate() }} href="/storages/{{ $storage->uuid }}" class="grid min-h-14 min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.8fr)_9rem] items-center border-b border-neutral-200 px-4 py-2.5 text-[12px] transition-colors last:border-b-0 hover:bg-neutral-50 hover:no-underline dark:border-white/[0.07] dark:hover:bg-white/[0.025]">
|
||||
<div class="truncate font-semibold text-black dark:text-fg">{{ $storage->name }}</div>
|
||||
<div class="truncate text-neutral-500 dark:text-fg-dim">{{ $storage->description ?: 'S3-compatible storage' }}</div>
|
||||
<div><x-status-badge :label="$storage->is_usable ? 'Connected' : 'Not usable'" :type="$storage->is_usable ? 'success' : 'error'" /></div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@include('livewire.shared.list-search-empty', ['label' => 'S3 storages'])
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -8,8 +8,8 @@
|
||||
x-data="{ open: false }" @keydown.escape.window="open = false">
|
||||
<button type="button" @click="open = !open" @click.outside="open = false"
|
||||
title="Switch team"
|
||||
class="group/team flex h-8 min-w-0 max-w-full items-center gap-1.5 rounded-lg px-2 -ml-1 text-left opacity-70 transition-[background-color,opacity] hover:bg-neutral-100 hover:opacity-100 dark:hover:bg-white/[0.05]">
|
||||
<span class="min-w-0 truncate text-[13px] font-semibold text-black dark:text-fg">{{ $currentTeam->name }}</span>
|
||||
class="group/team flex h-8 items-center gap-1.5 rounded-lg px-2 -ml-1 text-left opacity-70 transition-[background-color,opacity] hover:bg-neutral-100 hover:opacity-100 dark:hover:bg-white/[0.05]">
|
||||
<span class="whitespace-nowrap text-[13px] font-semibold text-black dark:text-fg">{{ $currentTeam->name }}</span>
|
||||
<svg class="size-4 shrink-0 text-neutral-400 dark:text-fg-faint" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M8 9l4-4 4 4M8 15l4 4 4-4" stroke="currentColor" stroke-width="1.6"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
|
||||
@@ -54,10 +54,28 @@
|
||||
description="Connect a Git provider to deploy applications directly from your repositories."
|
||||
icon-name="sources" />
|
||||
@else
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@php
|
||||
$items = $sources->map(function ($source) {
|
||||
$isGithub = $source->getMorphClass() === 'App\\Models\\GithubApp';
|
||||
return [
|
||||
'name' => $source->name,
|
||||
'provider' => $isGithub ? 'GitHub' : 'GitLab',
|
||||
'organization' => $isGithub ? $source->organization : $source->group_name,
|
||||
'status' => $source->isConnected() ? 'Connected' : 'Setup incomplete',
|
||||
];
|
||||
})->values();
|
||||
@endphp
|
||||
<div x-data="{
|
||||
search: '', viewMode: localStorage.getItem('coolify-sources-view') || 'table', items: @js($items),
|
||||
get filteredItems() { const query = this.search.trim().toLowerCase(); return query ? this.items.filter(item => Object.values(item).some(value => String(value || '').toLowerCase().includes(query))) : this.items; },
|
||||
matches(values) { const query = this.search.trim().toLowerCase(); return !query || values.some(value => String(value || '').toLowerCase().includes(query)); },
|
||||
setViewMode(mode) { this.viewMode = mode; localStorage.setItem('coolify-sources-view', mode); }
|
||||
}">
|
||||
@include('livewire.shared.list-search-controls', ['placeholder' => 'Search sources', 'singular' => 'source', 'plural' => 'sources'])
|
||||
<div x-cloak x-show="viewMode === 'grid'" class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@foreach ($sources as $source)
|
||||
@if ($source->getMorphClass() === 'App\Models\GithubApp')
|
||||
<a class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
<a x-show="matches(@js([$source->name, 'GitHub', $source->organization, $source->isConnected() ? 'Connected' : 'Setup incomplete']))" class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
{{ wireNavigate() }}
|
||||
href="{{ route('source.github.show', ['github_app_uuid' => data_get($source, 'uuid')]) }}">
|
||||
<div class="flex items-start gap-3">
|
||||
@@ -84,7 +102,7 @@
|
||||
</div>
|
||||
</a>
|
||||
@elseif ($source->getMorphClass() === 'App\Models\GitlabApp')
|
||||
<a class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
<a x-show="matches(@js([$source->name, 'GitLab', $source->group_name, $source->isConnected() ? 'Connected' : 'Setup incomplete']))" class="group flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
{{ wireNavigate() }}
|
||||
href="{{ route('source.gitlab.show', ['gitlab_app_uuid' => data_get($source, 'uuid')]) }}">
|
||||
<div class="flex items-start gap-3">
|
||||
@@ -113,6 +131,24 @@
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
<div x-show="viewMode === 'table'" class="overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
<div class="grid min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.7fr)_9rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint"><div>Source</div><div>Provider</div><div>Status</div></div>
|
||||
@foreach ($sources as $source)
|
||||
@php
|
||||
$isGithub = $source->getMorphClass() === 'App\\Models\\GithubApp';
|
||||
$provider = $isGithub ? 'GitHub' : 'GitLab';
|
||||
$organization = $isGithub ? $source->organization : $source->group_name;
|
||||
$href = $isGithub ? route('source.github.show', ['github_app_uuid' => $source->uuid]) : route('source.gitlab.show', ['gitlab_app_uuid' => $source->uuid]);
|
||||
@endphp
|
||||
<a x-show="matches(@js([$source->name, $provider, $organization, $source->isConnected() ? 'Connected' : 'Setup incomplete']))" {{ wireNavigate() }} href="{{ $href }}" class="grid min-h-14 min-w-[620px] grid-cols-[minmax(0,1fr)_minmax(10rem,.7fr)_9rem] items-center border-b border-neutral-200 px-4 py-2.5 text-[12px] transition-colors last:border-b-0 hover:bg-neutral-50 hover:no-underline dark:border-white/[0.07] dark:hover:bg-white/[0.025]">
|
||||
<div class="truncate font-semibold text-black dark:text-fg">{{ $source->name }}</div>
|
||||
<div class="truncate text-neutral-500 dark:text-fg-dim">{{ $organization ? "{$provider} · {$organization}" : $provider }}</div>
|
||||
<div><x-status-badge :label="$source->isConnected() ? 'Connected' : 'Setup incomplete'" :type="$source->isConnected() ? 'success' : 'warning'" /></div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@include('livewire.shared.list-search-empty', ['label' => 'sources'])
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-layout>
|
||||
Reference in new issue
Block a user