mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-14 06:17:36 +00:00
feat(ui): standardize paginated tables and responsive dropdowns
Add reusable pagination and page-size controls, improve mobile dropdown positioning, and migrate paginated views to consistent table components.
This commit is contained in:
1 parent
7bcdc23086
commit
0a35909ddf
32 files changed
+610
-624
No files matched your search
@@ -22,6 +22,14 @@ class Index extends Component
|
||||
|
||||
public int $defaultTake = 10;
|
||||
|
||||
public function updatedDefaultTake(): void
|
||||
{
|
||||
$this->defaultTake = max(1, min(100, $this->defaultTake));
|
||||
|
||||
$this->skip = 0;
|
||||
$this->loadDeployments();
|
||||
}
|
||||
|
||||
public bool $showNext = false;
|
||||
|
||||
public bool $showPrev = false;
|
||||
|
||||
@@ -44,6 +44,14 @@ class All extends Component
|
||||
|
||||
public int $perPage = 10;
|
||||
|
||||
public function updatedPerPage(): void
|
||||
{
|
||||
$this->perPage = max(1, min(100, $this->perPage));
|
||||
|
||||
$this->page = 1;
|
||||
$this->clearEnvironmentVariableCaches();
|
||||
}
|
||||
|
||||
public bool $is_env_sorting_enabled = false;
|
||||
|
||||
public bool $use_build_secrets = false;
|
||||
|
||||
@@ -58,6 +58,15 @@ class VolumeBackups extends Component
|
||||
|
||||
public int $timeout = 3600;
|
||||
|
||||
public int $perPage = 10;
|
||||
|
||||
public function updatedPerPage(): void
|
||||
{
|
||||
$this->perPage = max(1, min(100, $this->perPage));
|
||||
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public bool $delete_backup_s3 = false;
|
||||
|
||||
public Collection $availableS3Storages;
|
||||
@@ -316,7 +325,7 @@ class VolumeBackups extends Component
|
||||
|
||||
public function render()
|
||||
{
|
||||
$executions = $this->backup?->executions()->paginate(10);
|
||||
$executions = $this->backup?->executions()->paginate($this->perPage);
|
||||
|
||||
return view('livewire.project.shared.storages.volume-backups', [
|
||||
'executions' => $executions ?? collect(),
|
||||
|
||||
@@ -16,6 +16,8 @@ class AdminView extends Component
|
||||
|
||||
public string $sort = 'name_asc';
|
||||
|
||||
public int $perPage = 10;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (! isInstanceAdmin()) {
|
||||
@@ -42,6 +44,13 @@ class AdminView extends Component
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedPerPage(): void
|
||||
{
|
||||
$this->perPage = max(1, min(100, $this->perPage));
|
||||
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function submitSearch(): void
|
||||
{
|
||||
if (! isInstanceAdmin()) {
|
||||
@@ -103,7 +112,7 @@ class AdminView extends Component
|
||||
->when($this->sort === 'email_desc', fn ($query) => $query->orderByDesc('email'))
|
||||
->when($this->sort === 'name_asc', fn ($query) => $query->orderBy('name'))
|
||||
->orderBy('id')
|
||||
->paginate(10);
|
||||
->paginate($this->perPage);
|
||||
|
||||
return view('livewire.team.admin-view', [
|
||||
'users' => $users,
|
||||
|
||||
@@ -1939,6 +1939,28 @@ html[data-theme="custom"] textarea:disabled {
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.floating-dropdown-panel {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Older menus still render inside their page container. On mobile, keep
|
||||
those panels fully visible until they are migrated to the portaled
|
||||
dropdown component. Portaled panels carry their own fixed coordinates. */
|
||||
@media (max-width: 767px) {
|
||||
.listbox-panel:not([style*="position: fixed"]) {
|
||||
position: fixed !important;
|
||||
top: 50% !important;
|
||||
right: auto !important;
|
||||
left: 50% !important;
|
||||
width: max-content;
|
||||
max-width: calc(100vw - 1.5rem) !important;
|
||||
max-height: calc(100dvh - 1.5rem) !important;
|
||||
box-sizing: border-box;
|
||||
transform: translate(-50%, -50%) !important;
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.listbox-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
@props([
|
||||
'summary',
|
||||
'pageSizeModel',
|
||||
'storageKey',
|
||||
'options' => [10, 25, 50, 100],
|
||||
'previousAction' => 'page = Math.max(1, page - 1)',
|
||||
'nextAction' => 'page = Math.min(totalPages, page + 1)',
|
||||
'previousDisabled' => 'page === 1',
|
||||
'nextDisabled' => 'page >= totalPages',
|
||||
])
|
||||
|
||||
<footer {{ $attributes->class('flex min-h-11 items-center justify-between border-t border-neutral-200 px-4 text-[11px] text-neutral-500 dark:border-white/[0.08] dark:text-fg-faint') }}>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="inline-flex h-7 items-center whitespace-nowrap tabular-nums" x-text="{{ $summary }}"></span>
|
||||
<x-page-size-select :model="$pageSizeModel" :storage-key="$storageKey" :options="$options" />
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<button type="button" x-on:click="{{ $previousAction }}" x-bind:disabled="{{ $previousDisabled }}"
|
||||
class="flex size-7 items-center justify-center rounded-md border border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black disabled:pointer-events-none disabled:opacity-35 dark:border-white/[0.08] dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
aria-label="Previous page">
|
||||
<x-reicon name="arrow-right" class="size-3.5 rotate-180" />
|
||||
</button>
|
||||
<button type="button" x-on:click="{{ $nextAction }}" x-bind:disabled="{{ $nextDisabled }}"
|
||||
class="flex size-7 items-center justify-center rounded-md border border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black disabled:pointer-events-none disabled:opacity-35 dark:border-white/[0.08] dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
aria-label="Next page">
|
||||
<x-reicon name="arrow-right" class="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -25,13 +25,10 @@
|
||||
const triggerRect = this.$refs.trigger.getBoundingClientRect();
|
||||
const panelRect = this.$refs.panel.getBoundingClientRect();
|
||||
const viewportPadding = 8;
|
||||
let left = triggerRect.left;
|
||||
|
||||
if ((left + panelRect.width + viewportPadding) > window.innerWidth) {
|
||||
left = window.innerWidth - panelRect.width - viewportPadding;
|
||||
}
|
||||
|
||||
left = Math.max(viewportPadding, left);
|
||||
const left = Math.max(
|
||||
viewportPadding,
|
||||
(window.innerWidth - panelRect.width) / 2,
|
||||
);
|
||||
|
||||
let top = triggerRect.bottom + 4;
|
||||
const maxTop = window.innerHeight - panelRect.height - viewportPadding;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
'value' => null, // initial value when wire=false
|
||||
'disabled' => false,
|
||||
'tooltip' => true,
|
||||
'portal' => false,
|
||||
'portal' => true,
|
||||
'preserveValue' => false,
|
||||
])
|
||||
|
||||
@@ -90,19 +90,26 @@
|
||||
const gap = 4;
|
||||
const edge = 12;
|
||||
const triggerRect = trigger.getBoundingClientRect();
|
||||
const panelWidth = Math.max(triggerRect.width, panel.offsetWidth);
|
||||
const panelWidth = Math.min(
|
||||
Math.max(triggerRect.width, panel.offsetWidth),
|
||||
window.innerWidth - (edge * 2),
|
||||
);
|
||||
const panelHeight = Math.min(panel.scrollHeight, 256);
|
||||
const fitsBelow = window.innerHeight - triggerRect.bottom - gap >= panelHeight;
|
||||
const top = fitsBelow
|
||||
? triggerRect.bottom + gap
|
||||
: Math.max(edge, triggerRect.top - gap - panelHeight);
|
||||
const left = Math.min(
|
||||
Math.max(edge, triggerRect.left),
|
||||
window.innerWidth - panelWidth - edge,
|
||||
);
|
||||
const left = window.innerWidth < 768
|
||||
? (window.innerWidth - panelWidth) / 2
|
||||
: Math.min(
|
||||
Math.max(edge, triggerRect.left),
|
||||
window.innerWidth - panelWidth - edge,
|
||||
);
|
||||
|
||||
panel.style.top = `${top}px`;
|
||||
panel.style.left = `${left}px`;
|
||||
panel.style.width = `${panelWidth}px`;
|
||||
panel.style.maxWidth = `${window.innerWidth - (edge * 2)}px`;
|
||||
panel.style.minWidth = `${triggerRect.width}px`;
|
||||
this.positioned = true;
|
||||
}
|
||||
@@ -123,7 +130,7 @@
|
||||
@if ($portal)
|
||||
<template x-teleport="body">
|
||||
<div id="{{ $panelId }}" class="listbox-panel"
|
||||
style="position: fixed; z-index: 9999; visibility: hidden" x-show="open && positioned"
|
||||
style="position: fixed; z-index: 9999; visibility: hidden" x-show="open"
|
||||
x-cloak :style="{ visibility: positioned ? 'visible' : 'hidden' }"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 -translate-y-1 scale-[0.98]"
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
@props([
|
||||
'model',
|
||||
'livewire' => false,
|
||||
'options' => [10, 25, 50, 100],
|
||||
'storageKey' => null,
|
||||
])
|
||||
|
||||
<label class="mb-0! flex h-7 items-center gap-1.5 text-[11px] text-neutral-500 dark:text-fg-dim"
|
||||
x-data="{
|
||||
selectedPageSize: @js((int) $options[0]),
|
||||
customPageSize: @js((int) $options[0]),
|
||||
customizingPageSize: false,
|
||||
syncCustomOption(value) {
|
||||
const select = this.$refs.pageSizePreset;
|
||||
let customOption = select?.querySelector('[data-custom-page-size]');
|
||||
if (@js(array_map('intval', $options)).includes(Number(value))) {
|
||||
customOption?.remove();
|
||||
} else if (select) {
|
||||
if (!customOption) {
|
||||
customOption = document.createElement('option');
|
||||
customOption.dataset.customPageSize = '';
|
||||
select.insertBefore(customOption, select.lastElementChild);
|
||||
}
|
||||
customOption.value = String(value);
|
||||
customOption.textContent = String(value);
|
||||
}
|
||||
if (select) select.value = String(value);
|
||||
},
|
||||
applyPageSize(value) {
|
||||
const pageSizeValue = Math.min(100, Math.max(1, Number(value) || 1));
|
||||
this.selectedPageSize = pageSizeValue;
|
||||
this.customPageSize = pageSizeValue;
|
||||
this.customizingPageSize = false;
|
||||
this.$nextTick(() => {
|
||||
this.syncCustomOption(pageSizeValue);
|
||||
});
|
||||
@if ($livewire)
|
||||
$wire.set(@js($model), pageSizeValue);
|
||||
@else
|
||||
{{ $model }} = pageSizeValue;
|
||||
page = 1;
|
||||
@endif
|
||||
@if (filled($storageKey))
|
||||
localStorage.setItem(@js($storageKey), pageSizeValue);
|
||||
@endif
|
||||
}
|
||||
}"
|
||||
x-init="
|
||||
@if (filled($storageKey))
|
||||
const savedPageSize = Number(localStorage.getItem(@js($storageKey)));
|
||||
if (savedPageSize >= 1 && savedPageSize <= 100) applyPageSize(savedPageSize);
|
||||
@endif
|
||||
">
|
||||
<span class="hidden sm:inline">Rows</span>
|
||||
<span x-show="!customizingPageSize" class="relative inline-flex h-7 w-12 items-center">
|
||||
<select x-ref="pageSizePreset" aria-label="Items per page"
|
||||
x-on:change="$el.value === 'custom' ? (customizingPageSize = true, $nextTick(() => $refs.customPageSize.focus())) : applyPageSize($el.value)"
|
||||
class="peer absolute inset-0 z-10 h-7! w-12! cursor-pointer opacity-0">
|
||||
@foreach ($options as $option)
|
||||
<option value="{{ $option }}">{{ $option }}</option>
|
||||
@endforeach
|
||||
<option value="custom">Custom…</option>
|
||||
</select>
|
||||
<span
|
||||
class="pointer-events-none inline-flex h-7 w-12 items-center justify-between border-0 px-1 text-[11px]! leading-none! tabular-nums text-neutral-500 transition-colors peer-hover:text-black dark:text-fg-dim dark:peer-hover:text-fg">
|
||||
<span x-text="selectedPageSize"></span>
|
||||
<x-reicon name="chevron-down" class="size-3 text-neutral-400 dark:text-fg-faint" />
|
||||
</span>
|
||||
</span>
|
||||
<input x-cloak x-show="customizingPageSize" x-ref="customPageSize" x-model.number="customPageSize"
|
||||
x-on:keydown.enter.prevent="applyPageSize(customPageSize)" x-on:keydown.escape.prevent="customizingPageSize = false"
|
||||
x-on:blur="if (customizingPageSize) applyPageSize(customPageSize)" type="number" min="1" max="100" inputmode="numeric"
|
||||
aria-label="Custom items per page"
|
||||
class="mb-0! h-7! w-14! rounded-md! border-neutral-200! bg-transparent! px-1.5! py-0! text-[11px]! tabular-nums shadow-none! focus:border-neutral-300! focus:ring-0! dark:border-white/[0.08]! dark:text-fg-dim!" />
|
||||
</label>
|
||||
@@ -54,15 +54,13 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative" @click.outside="sortOpen = false" @keydown.escape.window="sortOpen = false">
|
||||
<button type="button" class="button" @click="sortOpen = !sortOpen">
|
||||
<x-table.dropdown panel-class="w-48!">
|
||||
<x-slot:trigger><button type="button" class="button" aria-haspopup="listbox" :aria-expanded="open">
|
||||
<x-reicon name="sort-direction" class="size-3.5" />
|
||||
Sort
|
||||
</button>
|
||||
<div x-show="sortOpen" x-cloak x-transition.opacity.duration.120ms
|
||||
class="listbox-panel left-auto! right-0! z-[90]! min-w-48!">
|
||||
</button></x-slot:trigger>
|
||||
<button type="button" class="listbox-option"
|
||||
@click="sharedSort = 'alphabetical'; sortOpen = false">
|
||||
@click="sharedSort = 'alphabetical'; close()">
|
||||
<span>Alphabetical</span>
|
||||
<svg x-show="sharedSort === 'alphabetical'" class="size-3.5 shrink-0" viewBox="0 0 24 24"
|
||||
fill="none">
|
||||
@@ -71,7 +69,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="listbox-option"
|
||||
@click="sharedSort = 'creation'; sortOpen = false">
|
||||
@click="sharedSort = 'creation'; close()">
|
||||
<span>Creation order</span>
|
||||
<svg x-show="sharedSort === 'creation'" class="size-3.5 shrink-0" viewBox="0 0 24 24"
|
||||
fill="none">
|
||||
@@ -79,8 +77,7 @@
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</x-table.dropdown>
|
||||
|
||||
@can('update', $resource)
|
||||
<x-modal-input title="New Shared Variable">
|
||||
|
||||
@@ -15,74 +15,43 @@
|
||||
@php
|
||||
$onFirstPage = (int) $currentPage <= 1;
|
||||
$onLastPage = (int) $currentPage >= (int) $lastPage;
|
||||
$navButtonClass =
|
||||
'flex w-10 items-center justify-center border-r border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 disabled:cursor-not-allowed disabled:opacity-35 dark:border-white/[0.10] dark:text-fg-dim dark:hover:bg-white/[0.06]';
|
||||
$lastNavButtonClass =
|
||||
'flex w-10 items-center justify-center text-neutral-500 transition-colors hover:bg-neutral-100 disabled:cursor-not-allowed disabled:opacity-35 dark:text-fg-dim dark:hover:bg-white/[0.06]';
|
||||
$buttonClass =
|
||||
'flex size-7 items-center justify-center rounded-md border border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black disabled:pointer-events-none disabled:opacity-35 dark:border-white/[0.08] dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg';
|
||||
$hasLoading = filled($wireTarget);
|
||||
@endphp
|
||||
|
||||
<div {{ $attributes->class('flex items-center justify-between gap-3 px-4 py-3') }}>
|
||||
<p class="shrink-0 whitespace-nowrap text-[13px] text-neutral-500 dark:text-fg-dim">
|
||||
Showing <span class="tabular-nums text-black dark:text-fg">{{ $from }}–{{ $to }}</span>
|
||||
of <span class="tabular-nums text-black dark:text-fg">{{ $total }}</span>
|
||||
</p>
|
||||
<div class="inline-flex h-8 overflow-hidden rounded-lg border border-neutral-200 dark:border-white/[0.10]">
|
||||
<button type="button" class="{{ $navButtonClass }}" aria-label="First page" title="First page"
|
||||
@if (filled($firstAction)) wire:click="{{ $firstAction }}" @endif
|
||||
@if ($hasLoading) wire:loading.attr="disabled" wire:target="{{ $wireTarget }}" @endif
|
||||
@disabled($onFirstPage)>
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M18 6L12 12L18 18M11 6L5 12L11 18" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="{{ $navButtonClass }}" aria-label="Previous page" title="Previous page"
|
||||
<footer {{ $attributes->class('flex min-h-11 items-center justify-between border-t border-neutral-200 px-4 text-[11px] text-neutral-500 dark:border-white/[0.08] dark:text-fg-faint') }}>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="inline-flex h-7 items-center whitespace-nowrap tabular-nums">{{ $from }}–{{ $to }} of {{ $total }}</span>
|
||||
@isset($pageSize)
|
||||
{{ $pageSize }}
|
||||
@endisset
|
||||
@if ($hasLoading)
|
||||
<span wire:loading.inline-flex wire:target="{{ $wireTarget }}" class="inline-flex items-center"
|
||||
aria-live="polite">
|
||||
<svg class="loading-indicator size-3.5 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||
</path>
|
||||
</svg>
|
||||
<span class="sr-only">Loading page…</span>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<button type="button" class="{{ $buttonClass }}" aria-label="Previous page"
|
||||
@if (filled($previousAction)) wire:click="{{ $previousAction }}" @endif
|
||||
@if ($hasLoading) wire:loading.attr="disabled" wire:target="{{ $wireTarget }}" @endif
|
||||
@disabled($onFirstPage)>
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M15 6L9 12L15 18" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
<x-reicon name="arrow-right" class="size-3.5 rotate-180" />
|
||||
</button>
|
||||
<span
|
||||
class="relative flex min-w-12 items-center justify-center border-r border-neutral-200 px-3 text-[13px] tabular-nums text-black dark:border-white/[0.10] dark:text-fg">
|
||||
@if ($hasLoading)
|
||||
<span wire:loading.remove wire:target="{{ $wireTarget }}">{{ $currentPage }}</span>
|
||||
<span wire:loading.inline-flex wire:target="{{ $wireTarget }}" class="inline-flex items-center"
|
||||
aria-live="polite">
|
||||
<svg class="loading-indicator size-3.5 animate-spin"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
|
||||
stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||
</path>
|
||||
</svg>
|
||||
<span class="sr-only">Loading page…</span>
|
||||
</span>
|
||||
@else
|
||||
{{ $currentPage }}
|
||||
@endif
|
||||
</span>
|
||||
<button type="button" class="{{ $navButtonClass }}" aria-label="Next page" title="Next page"
|
||||
<button type="button" class="{{ $buttonClass }}" aria-label="Next page"
|
||||
@if (filled($nextAction)) wire:click="{{ $nextAction }}" @endif
|
||||
@if ($hasLoading) wire:loading.attr="disabled" wire:target="{{ $wireTarget }}" @endif
|
||||
@disabled($onLastPage)>
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M9 6L15 12L9 18" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="{{ $lastNavButtonClass }}" aria-label="Last page" title="Last page"
|
||||
@if (filled($lastAction)) wire:click="{{ $lastAction }}" @endif
|
||||
@if ($hasLoading) wire:loading.attr="disabled" wire:target="{{ $wireTarget }}" @endif
|
||||
@disabled($onLastPage)>
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M6 6L12 12L6 18M13 6L19 12L13 18" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<x-reicon name="arrow-right" class="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -0,0 +1,68 @@
|
||||
@props([
|
||||
'panelClass' => '',
|
||||
'role' => 'listbox',
|
||||
'multiselectable' => false,
|
||||
])
|
||||
|
||||
@php($panelId = 'table-dropdown-'.uniqid())
|
||||
|
||||
<div class="relative" x-data="{
|
||||
open: false,
|
||||
positioned: false,
|
||||
toggle() {
|
||||
this.open = !this.open;
|
||||
this.positioned = false;
|
||||
|
||||
if (this.open) {
|
||||
this.$nextTick(() => requestAnimationFrame(() => this.positionPanel()));
|
||||
}
|
||||
},
|
||||
close() {
|
||||
this.open = false;
|
||||
this.positioned = false;
|
||||
},
|
||||
positionPanel() {
|
||||
const trigger = this.$refs.trigger;
|
||||
const panel = document.getElementById(@js($panelId));
|
||||
|
||||
if (!trigger || !panel) return;
|
||||
|
||||
const gap = 4;
|
||||
const edge = 12;
|
||||
const triggerRect = trigger.getBoundingClientRect();
|
||||
const panelWidth = Math.min(panel.offsetWidth, window.innerWidth - (edge * 2));
|
||||
const panelHeight = Math.min(panel.scrollHeight, window.innerHeight - (edge * 2));
|
||||
const fitsBelow = window.innerHeight - triggerRect.bottom - gap >= panelHeight;
|
||||
const top = fitsBelow
|
||||
? triggerRect.bottom + gap
|
||||
: Math.max(edge, triggerRect.top - gap - panelHeight);
|
||||
const left = window.innerWidth < 768
|
||||
? (window.innerWidth - panelWidth) / 2
|
||||
: Math.min(
|
||||
Math.max(edge, triggerRect.right - panelWidth),
|
||||
window.innerWidth - panelWidth - edge,
|
||||
);
|
||||
|
||||
panel.style.top = `${top}px`;
|
||||
panel.style.left = `${left}px`;
|
||||
panel.style.width = `${panelWidth}px`;
|
||||
panel.style.maxWidth = `${window.innerWidth - (edge * 2)}px`;
|
||||
panel.style.maxHeight = `${window.innerHeight - (edge * 2)}px`;
|
||||
this.positioned = true;
|
||||
}
|
||||
}" @click.outside="close()" @keydown.escape.window="close()"
|
||||
@resize.window="open && positionPanel()" @scroll.window="open && positionPanel()">
|
||||
<div x-ref="trigger" @click="toggle()">
|
||||
{{ $trigger }}
|
||||
</div>
|
||||
|
||||
<template x-teleport="body">
|
||||
<div id="{{ $panelId }}" x-show="open" x-cloak
|
||||
class="listbox-panel floating-dropdown-panel z-[9999]! {{ $panelClass }}" style="position: fixed; visibility: hidden"
|
||||
:style="{ visibility: positioned ? 'visible' : 'hidden' }"
|
||||
x-effect="if (open) requestAnimationFrame(() => positionPanel())" role="{{ $role }}"
|
||||
@if ($multiselectable) aria-multiselectable="true" @endif>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -1,24 +1,25 @@
|
||||
@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="table-filter">
|
||||
<x-table.dropdown panel-class="w-44! overflow-hidden! p-0!" :multiselectable="true">
|
||||
<x-slot:trigger>
|
||||
<button type="button" 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>
|
||||
</x-slot:trigger>
|
||||
<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)>
|
||||
wire:click="{{ $resetAction }}" @click="close()" @disabled($activeCount === 0)>
|
||||
<span>{{ $resetLabel }}</span>
|
||||
<x-reicon name="x" class="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</x-table.dropdown>
|
||||
</div>
|
||||
@@ -1,10 +1,11 @@
|
||||
<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">
|
||||
<div class="table-sort">
|
||||
<x-table.dropdown panel-class="w-44!">
|
||||
<x-slot:trigger>
|
||||
<button type="button" class="button" aria-haspopup="listbox" :aria-expanded="open">
|
||||
<x-reicon name="sort-direction" class="size-3.5" />
|
||||
Sort
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</x-table.dropdown>
|
||||
</div>
|
||||
@@ -111,20 +111,18 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative" x-on:click.outside="filterOpen = false">
|
||||
<button type="button" class="button" x-on:click="filterOpen = !filterOpen; sortOpen = false">
|
||||
<x-table.dropdown panel-class="w-48!">
|
||||
<x-slot:trigger><button type="button" class="button" aria-haspopup="listbox" :aria-expanded="open">
|
||||
<svg class="size-3.5 opacity-65" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M4 6h16M7 12h10M10 18h4" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" />
|
||||
</svg>
|
||||
Filter
|
||||
</button>
|
||||
<div x-cloak x-show="filterOpen" x-transition.origin.top.right
|
||||
class="absolute top-9 right-0 z-50 min-w-48 rounded-lg border border-neutral-200 bg-white p-1 shadow-modal dark:border-white/[0.1] dark:bg-raised">
|
||||
</button></x-slot:trigger>
|
||||
<template x-for="option in filterOptions" :key="option.value">
|
||||
<button type="button"
|
||||
class="flex h-8 w-full items-center rounded-md px-2 text-left text-[12px] text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
x-on:click="typeFilter = option.value; filterOpen = false">
|
||||
x-on:click="typeFilter = option.value; close()">
|
||||
<span class="flex-1" x-text="option.label"></span>
|
||||
<svg x-show="typeFilter === option.value" class="size-3.5 text-warning"
|
||||
viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
@@ -133,23 +131,20 @@
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</x-table.dropdown>
|
||||
|
||||
<div class="relative" x-on:click.outside="sortOpen = false">
|
||||
<button type="button" class="button" x-on:click="sortOpen = !sortOpen; filterOpen = false">
|
||||
<x-table.dropdown panel-class="w-48!">
|
||||
<x-slot:trigger><button type="button" class="button" aria-haspopup="listbox" :aria-expanded="open">
|
||||
<svg class="size-3.5 opacity-65" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M8 5v14m0 0-3-3m3 3 3-3M16 19V5m0 0-3 3m3-3 3 3" stroke="currentColor"
|
||||
stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
Sort
|
||||
</button>
|
||||
<div x-cloak x-show="sortOpen" x-transition.origin.top.right
|
||||
class="absolute top-9 right-0 z-50 min-w-48 rounded-lg border border-neutral-200 bg-white p-1 shadow-modal dark:border-white/[0.1] dark:bg-raised">
|
||||
</button></x-slot:trigger>
|
||||
<template x-for="option in sortOptions" :key="option.value">
|
||||
<button type="button"
|
||||
class="flex h-8 w-full items-center rounded-md px-2 text-left text-[12px] text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
x-on:click="sortBy = option.value; sortOpen = false">
|
||||
x-on:click="sortBy = option.value; close()">
|
||||
<span class="flex-1" x-text="option.label"></span>
|
||||
<svg x-show="sortBy === option.value" class="size-3.5 text-warning"
|
||||
viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
@@ -158,8 +153,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</x-table.dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -253,7 +253,13 @@
|
||||
:current-page="$currentPage" :last-page="$lastPage"
|
||||
wire-target="goToPage,previousPage,nextPage" first-action="goToPage(1)"
|
||||
previous-action="previousPage" next-action="nextPage"
|
||||
last-action="goToPage({{ $lastPage }})" />
|
||||
last-action="goToPage({{ $lastPage }})">
|
||||
@unless ($embedded)
|
||||
<x-slot:pageSize>
|
||||
<x-page-size-select model="defaultTake" livewire storage-key="coolify.page-size.deployments" />
|
||||
</x-slot:pageSize>
|
||||
@endunless
|
||||
</x-table-pagination>
|
||||
</div>
|
||||
@else
|
||||
<x-empty size="sm" title="No deployments found"
|
||||
|
||||
@@ -49,22 +49,22 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative" x-on:click.outside="sortOpen = false">
|
||||
<button type="button" class="button" x-on:click="sortOpen = !sortOpen">
|
||||
<svg class="size-3.5 opacity-65" viewBox="0 0 24 24" fill="none"
|
||||
aria-hidden="true">
|
||||
<path d="M8 5v14m0 0-3-3m3 3 3-3M16 19V5m0 0-3 3m3-3 3 3"
|
||||
stroke="currentColor" stroke-width="1.7" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
Sort
|
||||
</button>
|
||||
<div x-cloak x-show="sortOpen" x-transition.origin.top.right
|
||||
class="absolute top-9 right-0 z-50 w-52 rounded-lg border border-neutral-200 bg-white p-1 shadow-modal dark:border-white/[0.1] dark:bg-raised">
|
||||
<x-table.dropdown panel-class="w-52!">
|
||||
<x-slot:trigger>
|
||||
<button type="button" class="button" aria-haspopup="listbox" :aria-expanded="open">
|
||||
<svg class="size-3.5 opacity-65" viewBox="0 0 24 24" fill="none"
|
||||
aria-hidden="true">
|
||||
<path d="M8 5v14m0 0-3-3m3 3 3-3M16 19V5m0 0-3 3m3-3 3 3"
|
||||
stroke="currentColor" stroke-width="1.7" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
Sort
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
<template x-for="option in sortOptions" :key="option.value">
|
||||
<button type="button"
|
||||
class="flex h-9 w-full items-center rounded-md px-2 text-left text-[12px] text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
x-on:click="sortBy = option.value; sortOpen = false; page = 1">
|
||||
x-on:click="sortBy = option.value; close(); page = 1">
|
||||
<span class="flex-1" x-text="option.label"></span>
|
||||
<svg x-show="sortBy === option.value" class="size-3.5 text-warning"
|
||||
viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
@@ -73,8 +73,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</x-table.dropdown>
|
||||
|
||||
<div
|
||||
class="flex h-9 items-center rounded-lg border border-neutral-200 bg-white p-0.5 dark:border-white/[0.08] dark:bg-white/[0.06]">
|
||||
@@ -152,31 +151,9 @@
|
||||
</article>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<footer x-show="totalPages > 1"
|
||||
class="mt-3 flex min-h-11 items-center justify-between rounded-xl border border-neutral-200 bg-white px-4 text-[11px] text-neutral-500 shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
<span x-text="`${rangeStart}-${rangeEnd} of ${filteredProjects.length}`"></span>
|
||||
<div class="flex items-center gap-1">
|
||||
<button type="button" x-on:click="previousPage" :disabled="page === 1"
|
||||
class="flex size-7 items-center justify-center rounded-md border border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black disabled:pointer-events-none disabled:opacity-35 dark:border-white/[0.08] dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
aria-label="Previous page">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none"
|
||||
aria-hidden="true">
|
||||
<path d="m15 5-7 7 7 7" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" x-on:click="nextPage" :disabled="page >= totalPages"
|
||||
class="flex size-7 items-center justify-center rounded-md border border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black disabled:pointer-events-none disabled:opacity-35 dark:border-white/[0.08] dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
aria-label="Next page">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none"
|
||||
aria-hidden="true">
|
||||
<path d="m9 5 7 7-7 7" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
<x-client-pagination x-show="filteredProjects.length > 0" class="mt-3 rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]"
|
||||
summary="`${rangeStart}-${rangeEnd} of ${filteredProjects.length}`" page-size-model="pageSize"
|
||||
storage-key="coolify.page-size.projects" :options="[12, 24, 48, 96]" />
|
||||
</div>
|
||||
|
||||
<div x-show="viewMode === 'table'"
|
||||
@@ -229,31 +206,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<footer x-show="totalPages > 1"
|
||||
class="flex min-h-11 items-center justify-between border-t border-neutral-200 px-4 text-[11px] text-neutral-500 dark:border-white/[0.08] dark:text-fg-faint">
|
||||
<span x-text="`${rangeStart}-${rangeEnd} of ${filteredProjects.length}`"></span>
|
||||
<div class="flex items-center gap-1">
|
||||
<button type="button" x-on:click="previousPage" :disabled="page === 1"
|
||||
class="flex size-7 items-center justify-center rounded-md border border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black disabled:pointer-events-none disabled:opacity-35 dark:border-white/[0.08] dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
aria-label="Previous page">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none"
|
||||
aria-hidden="true">
|
||||
<path d="m15 5-7 7 7 7" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" x-on:click="nextPage" :disabled="page >= totalPages"
|
||||
class="flex size-7 items-center justify-center rounded-md border border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black disabled:pointer-events-none disabled:opacity-35 dark:border-white/[0.08] dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
aria-label="Next page">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none"
|
||||
aria-hidden="true">
|
||||
<path d="m9 5 7 7-7 7" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
<x-client-pagination x-show="filteredProjects.length > 0"
|
||||
summary="`${rangeStart}-${rangeEnd} of ${filteredProjects.length}`" page-size-model="pageSize"
|
||||
storage-key="coolify.page-size.projects" :options="[12, 24, 48, 96]" />
|
||||
</div>
|
||||
|
||||
<div x-show="filteredProjects.length === 0"
|
||||
|
||||
@@ -20,17 +20,12 @@
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<div class="relative" @click.outside="filterOpen = false">
|
||||
<button type="button" class="button" @click="filterOpen = !filterOpen"
|
||||
aria-haspopup="listbox" aria-controls="resource-type-filter-options"
|
||||
:aria-expanded="filterOpen">
|
||||
<x-table.dropdown panel-class="w-48!">
|
||||
<x-slot:trigger><button type="button" class="button"
|
||||
aria-haspopup="listbox" :aria-expanded="open">
|
||||
<x-reicon name="filter" class="size-3.5" />
|
||||
Filter
|
||||
</button>
|
||||
<div id="resource-type-filter-options" x-show="filterOpen" x-cloak
|
||||
x-transition.opacity.duration.120ms role="listbox" aria-label="Resource type"
|
||||
@keydown.escape.stop="filterOpen = false; $el.previousElementSibling.focus()"
|
||||
class="listbox-panel left-auto! right-0! z-[90]! min-w-48!">
|
||||
</button></x-slot:trigger>
|
||||
<div
|
||||
class="px-2 py-1 text-[10px] font-semibold tracking-wide text-neutral-400 uppercase dark:text-fg-faint">
|
||||
Resource type
|
||||
@@ -38,14 +33,13 @@
|
||||
<template x-for="option in resourceTypeOptions" :key="option.value">
|
||||
<button type="button" class="listbox-option" role="option"
|
||||
:aria-selected="resourceType === option.value"
|
||||
@click="resourceType = option.value; filterOpen = false">
|
||||
@click="resourceType = option.value; close()">
|
||||
<span x-text="option.label"></span>
|
||||
<x-reicon name="check-circle" class="size-3.5 text-accent"
|
||||
x-show="resourceType === option.value" />
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</x-table.dropdown>
|
||||
|
||||
<div class="relative w-48" @click.outside="closeCategoryFilter()">
|
||||
<button type="button" class="listbox-trigger"
|
||||
|
||||
@@ -67,10 +67,12 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative" x-on:click.outside="filterOpen = false">
|
||||
<button type="button" class="button max-w-64"
|
||||
:class="activeFilterCount > 0 && 'button-highlighted'"
|
||||
x-on:click="filterOpen = !filterOpen" :title="activeFilterCount > 0 ? filterButtonText : 'Filter'">
|
||||
<x-table.dropdown panel-class="w-64! overflow-hidden! p-0!" :multiselectable="true">
|
||||
<x-slot:trigger>
|
||||
<button type="button" class="button max-w-64"
|
||||
:class="activeFilterCount > 0 && 'button-highlighted'"
|
||||
:title="activeFilterCount > 0 ? filterButtonText : 'Filter'"
|
||||
aria-haspopup="listbox" :aria-expanded="open">
|
||||
<svg class="size-3.5 opacity-65" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M4 6h16M7 12h10M10 18h4" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" />
|
||||
@@ -79,9 +81,8 @@
|
||||
<span x-show="activeFilterCount > 0"
|
||||
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"
|
||||
x-text="activeFilterCount"></span>
|
||||
</button>
|
||||
<div x-cloak x-show="filterOpen" x-transition.origin.top.right
|
||||
class="absolute top-9 right-0 z-50 flex w-64 flex-col overflow-hidden rounded-lg border border-neutral-200 bg-white shadow-modal dark:border-white/[0.1] dark:bg-raised">
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
<div class="max-h-80 overflow-y-auto p-1">
|
||||
<template x-for="group in filterGroups" :key="group.key">
|
||||
<div x-show="group.options.length > 0">
|
||||
@@ -113,24 +114,23 @@
|
||||
Clear filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-table.dropdown>
|
||||
|
||||
<div class="relative" x-on:click.outside="sortOpen = false">
|
||||
<button type="button" class="button" x-on:click="sortOpen = !sortOpen">
|
||||
<x-table.dropdown panel-class="w-48!">
|
||||
<x-slot:trigger>
|
||||
<button type="button" class="button" aria-haspopup="listbox" :aria-expanded="open">
|
||||
<svg class="size-3.5 opacity-65" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M8 5v14m0 0-3-3m3 3 3-3M16 19V5m0 0-3 3m3-3 3 3"
|
||||
stroke="currentColor" stroke-width="1.7" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
Sort
|
||||
</button>
|
||||
<div x-cloak x-show="sortOpen" x-transition.origin.top.right
|
||||
class="absolute top-9 right-0 z-50 w-48 rounded-lg border border-neutral-200 bg-white p-1 shadow-modal dark:border-white/[0.1] dark:bg-raised">
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
<template x-for="option in sortOptions" :key="option.value">
|
||||
<button type="button"
|
||||
class="flex h-9 w-full items-center rounded-md px-2 text-left text-[12px] text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
x-on:click="sortBy = option.value; sortOpen = false; page = 1">
|
||||
x-on:click="sortBy = option.value; close(); page = 1">
|
||||
<span class="flex-1" x-text="option.label"></span>
|
||||
<svg x-show="sortBy === option.value" class="size-3.5 text-warning"
|
||||
viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
@@ -139,8 +139,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</x-table.dropdown>
|
||||
|
||||
<div
|
||||
class="flex h-9 items-center rounded-lg border border-neutral-200 bg-white p-0.5 dark:border-white/[0.08] dark:bg-white/[0.06]">
|
||||
@@ -263,30 +262,9 @@
|
||||
Try a different search or filter.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<footer x-show="totalPages > 1"
|
||||
class="flex min-h-11 items-center justify-between border-t border-neutral-200 px-4 text-[11px] text-neutral-500 dark:border-white/[0.08] dark:text-fg-faint">
|
||||
<span
|
||||
x-text="filteredResources.length === 0 ? '0 resources' : `${rangeStart}-${rangeEnd} of ${filteredResources.length}`"></span>
|
||||
<div class="flex items-center gap-1">
|
||||
<button type="button" x-on:click="previousPage" :disabled="page === 1"
|
||||
class="flex size-7 items-center justify-center rounded-md border border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black disabled:pointer-events-none disabled:opacity-35 dark:border-white/[0.08] dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
aria-label="Previous page">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="m15 5-7 7 7 7" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" x-on:click="nextPage" :disabled="page >= totalPages"
|
||||
class="flex size-7 items-center justify-center rounded-md border border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black disabled:pointer-events-none disabled:opacity-35 dark:border-white/[0.08] dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
aria-label="Next page">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="m9 5 7 7-7 7" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
<x-client-pagination x-show="filteredResources.length > 0"
|
||||
summary="filteredResources.length === 0 ? '0 resources' : `${rangeStart}-${rangeEnd} of ${filteredResources.length}`"
|
||||
page-size-model="pageSize" storage-key="coolify.page-size.environment-resources" />
|
||||
</div>
|
||||
|
||||
<div x-cloak x-show="viewMode === 'grid'">
|
||||
@@ -351,32 +329,10 @@
|
||||
Try a different search or filter.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<footer x-show="totalPages > 1"
|
||||
class="mt-3 flex min-h-11 items-center justify-between rounded-xl border border-neutral-200 bg-white px-4 text-[11px] text-neutral-500 shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
<span
|
||||
x-text="filteredResources.length === 0 ? '0 resources' : `${rangeStart}-${rangeEnd} of ${filteredResources.length}`"></span>
|
||||
<div class="flex items-center gap-1">
|
||||
<button type="button" x-on:click="previousPage" :disabled="page === 1"
|
||||
class="flex size-7 items-center justify-center rounded-md border border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black disabled:pointer-events-none disabled:opacity-35 dark:border-white/[0.08] dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
aria-label="Previous page">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none"
|
||||
aria-hidden="true">
|
||||
<path d="m15 5-7 7 7 7" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" x-on:click="nextPage" :disabled="page >= totalPages"
|
||||
class="flex size-7 items-center justify-center rounded-md border border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black disabled:pointer-events-none disabled:opacity-35 dark:border-white/[0.08] dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
aria-label="Next page">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none"
|
||||
aria-hidden="true">
|
||||
<path d="m9 5 7 7-7 7" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
<x-client-pagination x-show="filteredResources.length > 0"
|
||||
class="mt-3 rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]"
|
||||
summary="filteredResources.length === 0 ? '0 resources' : `${rangeStart}-${rangeEnd} of ${filteredResources.length}`"
|
||||
page-size-model="pageSize" storage-key="coolify.page-size.environment-resources" />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -143,20 +143,18 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative" x-on:click.outside="filterOpen = false">
|
||||
<button type="button" class="button" x-on:click="filterOpen = !filterOpen; sortOpen = false">
|
||||
<x-table.dropdown panel-class="w-48!">
|
||||
<x-slot:trigger><button type="button" class="button" aria-haspopup="listbox" :aria-expanded="open">
|
||||
<svg class="size-3.5 opacity-65" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M4 6h16M7 12h10M10 18h4" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" />
|
||||
</svg>
|
||||
Filter
|
||||
</button>
|
||||
<div x-cloak x-show="filterOpen" x-transition.origin.top.right
|
||||
class="absolute top-9 right-0 z-50 min-w-48 rounded-lg border border-neutral-200 bg-white p-1 shadow-modal dark:border-white/[0.1] dark:bg-raised">
|
||||
</button></x-slot:trigger>
|
||||
<template x-for="option in filterOptions" :key="option.value">
|
||||
<button type="button"
|
||||
class="flex h-8 w-full items-center rounded-md px-2 text-left text-[12px] text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
x-on:click="typeFilter = option.value; filterOpen = false">
|
||||
x-on:click="typeFilter = option.value; close()">
|
||||
<span class="flex-1" x-text="option.label"></span>
|
||||
<svg x-show="typeFilter === option.value" class="size-3.5 text-warning"
|
||||
viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
@@ -165,23 +163,20 @@
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</x-table.dropdown>
|
||||
|
||||
<div class="relative" x-on:click.outside="sortOpen = false">
|
||||
<button type="button" class="button" x-on:click="sortOpen = !sortOpen; filterOpen = false">
|
||||
<x-table.dropdown panel-class="w-48!">
|
||||
<x-slot:trigger><button type="button" class="button" aria-haspopup="listbox" :aria-expanded="open">
|
||||
<svg class="size-3.5 opacity-65" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M8 5v14m0 0-3-3m3 3 3-3M16 19V5m0 0-3 3m3-3 3 3" stroke="currentColor"
|
||||
stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
Sort
|
||||
</button>
|
||||
<div x-cloak x-show="sortOpen" x-transition.origin.top.right
|
||||
class="absolute top-9 right-0 z-50 min-w-48 rounded-lg border border-neutral-200 bg-white p-1 shadow-modal dark:border-white/[0.1] dark:bg-raised">
|
||||
</button></x-slot:trigger>
|
||||
<template x-for="option in sortOptions" :key="option.value">
|
||||
<button type="button"
|
||||
class="flex h-8 w-full items-center rounded-md px-2 text-left text-[12px] text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
x-on:click="sortBy = option.value; sortOpen = false">
|
||||
x-on:click="sortBy = option.value; close()">
|
||||
<span class="flex-1" x-text="option.label"></span>
|
||||
<svg x-show="sortBy === option.value" class="size-3.5 text-warning"
|
||||
viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
@@ -190,8 +185,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</x-table.dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -233,7 +233,11 @@
|
||||
first-action="setEnvironmentVariablePage(1)"
|
||||
previous-action="previousEnvironmentVariablePage"
|
||||
next-action="nextEnvironmentVariablePage"
|
||||
last-action="setEnvironmentVariablePage({{ $lastPage }})" />
|
||||
last-action="setEnvironmentVariablePage({{ $lastPage }})">
|
||||
<x-slot:pageSize>
|
||||
<x-page-size-select model="perPage" livewire storage-key="coolify.page-size.environment-variables" />
|
||||
</x-slot:pageSize>
|
||||
</x-table-pagination>
|
||||
</div>
|
||||
@else
|
||||
<div class="relative min-h-40">
|
||||
|
||||
@@ -405,24 +405,16 @@
|
||||
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 order-4 shrink-0">
|
||||
<button x-on:click="filterOpen = !filterOpen" title="Filter Log Levels"
|
||||
<x-table.dropdown panel-class="runtime-log-menu min-w-40!">
|
||||
<x-slot:trigger><button type="button" title="Filter Log Levels"
|
||||
:class="Object.values(logFilters).some(v => !v) ? 'runtime-log-icon-button-active' : ''"
|
||||
class="runtime-log-icon-button">
|
||||
class="runtime-log-icon-button" aria-haspopup="listbox" :aria-expanded="open">
|
||||
<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="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 0 1-.659 1.591l-5.432 5.432a2.25 2.25 0 0 0-.659 1.591v2.927a2.25 2.25 0 0 1-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 0 0-.659-1.591L3.659 7.409A2.25 2.25 0 0 1 3 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0 1 12 3Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<div x-show="filterOpen" x-on:click.away="filterOpen = false"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="transform opacity-0 scale-95"
|
||||
x-transition:enter-end="transform opacity-100 scale-100"
|
||||
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="runtime-log-menu listbox-panel left-auto! right-0! z-[90]! min-w-40!">
|
||||
</button></x-slot:trigger>
|
||||
<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>
|
||||
@@ -445,8 +437,7 @@
|
||||
<span x-show="logFilters.info">✓</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-table.dropdown>
|
||||
<button title="Follow Logs" :class="alwaysScroll ? 'runtime-log-icon-button-active' : ''"
|
||||
x-on:click="toggleScroll"
|
||||
class="runtime-log-icon-button order-2">
|
||||
|
||||
+8
-24
@@ -133,30 +133,14 @@
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
<footer
|
||||
class="flex min-h-11 items-center justify-between border-t border-neutral-200 px-4 text-[11px] text-neutral-500 dark:border-white/[0.08] dark:text-fg-faint">
|
||||
<span>{{ $executionCount }} {{ Str::plural('execution', $executionCount) }}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<button type="button" wire:click="previousPage" @disabled($currentPage === 1)
|
||||
class="flex size-7 items-center justify-center rounded-md border border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 disabled:pointer-events-none disabled:opacity-35 dark:border-white/[0.08] dark:text-fg-dim dark:hover:bg-white/[0.06]"
|
||||
aria-label="Previous page">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none">
|
||||
<path d="m15 5-7 7 7 7" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<span class="min-w-14 text-center tabular-nums">{{ $currentPage }} / {{ $lastPage }}</span>
|
||||
<button type="button" wire:click="nextPage" @disabled($currentPage === $lastPage)
|
||||
class="flex size-7 items-center justify-center rounded-md border border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 disabled:pointer-events-none disabled:opacity-35 dark:border-white/[0.08] dark:text-fg-dim dark:hover:bg-white/[0.06]"
|
||||
aria-label="Next page">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none">
|
||||
<path d="m9 5 7 7-7 7" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
<x-table-pagination :from="$executions->firstItem() ?? 0" :to="$executions->lastItem() ?? 0"
|
||||
:total="$executionCount" :current-page="$currentPage" :last-page="$lastPage"
|
||||
wire-target="previousPage,nextPage" previous-action="previousPage" next-action="nextPage">
|
||||
<x-slot:pageSize>
|
||||
<x-page-size-select model="perPage" livewire
|
||||
storage-key="coolify.page-size.volume-backup-executions" />
|
||||
</x-slot:pageSize>
|
||||
</x-table-pagination>
|
||||
</div>
|
||||
@else
|
||||
<x-empty size="sm" title="No executions"
|
||||
|
||||
@@ -69,8 +69,9 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative" x-on:click.outside="sortOpen = false">
|
||||
<button type="button" class="button" x-on:click="sortOpen = !sortOpen">
|
||||
<x-table.dropdown panel-class="w-48!">
|
||||
<x-slot:trigger>
|
||||
<button type="button" class="button" aria-haspopup="listbox" :aria-expanded="open">
|
||||
<svg class="size-3.5 opacity-65" viewBox="0 0 24 24" fill="none"
|
||||
aria-hidden="true">
|
||||
<path d="M8 5v14m0 0-3-3m3 3 3-3M16 19V5m0 0-3 3m3-3 3 3"
|
||||
@@ -78,13 +79,12 @@
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
Sort
|
||||
</button>
|
||||
<div x-cloak x-show="sortOpen" x-transition.origin.top.right
|
||||
class="absolute top-9 right-0 z-50 w-48 rounded-lg border border-neutral-200 bg-white p-1 shadow-modal dark:border-white/[0.1] dark:bg-raised">
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
<template x-for="option in sortOptions" :key="option.value">
|
||||
<button type="button"
|
||||
class="flex h-9 w-full items-center rounded-md px-2 text-left text-[12px] text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
x-on:click="sortBy = option.value; sortOpen = false; page = 1">
|
||||
x-on:click="sortBy = option.value; close(); page = 1">
|
||||
<span class="flex-1" x-text="option.label"></span>
|
||||
<svg x-show="sortBy === option.value" class="size-3.5 text-warning"
|
||||
viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
@@ -93,8 +93,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</x-table.dropdown>
|
||||
|
||||
<div
|
||||
class="flex h-9 items-center rounded-lg border border-neutral-200 bg-white p-0.5 dark:border-white/[0.08] dark:bg-white/[0.06]">
|
||||
@@ -166,31 +165,9 @@
|
||||
</article>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<footer x-show="totalPages > 1"
|
||||
class="mt-3 flex min-h-11 items-center justify-between rounded-xl border border-neutral-200 bg-white px-4 text-[11px] text-neutral-500 shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
<span x-text="`${rangeStart}-${rangeEnd} of ${filteredEnvironments.length}`"></span>
|
||||
<div class="flex items-center gap-1">
|
||||
<button type="button" x-on:click="previousPage" :disabled="page === 1"
|
||||
class="flex size-7 items-center justify-center rounded-md border border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black disabled:pointer-events-none disabled:opacity-35 dark:border-white/[0.08] dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
aria-label="Previous page">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none"
|
||||
aria-hidden="true">
|
||||
<path d="m15 5-7 7 7 7" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" x-on:click="nextPage" :disabled="page >= totalPages"
|
||||
class="flex size-7 items-center justify-center rounded-md border border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black disabled:pointer-events-none disabled:opacity-35 dark:border-white/[0.08] dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
aria-label="Next page">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none"
|
||||
aria-hidden="true">
|
||||
<path d="m9 5 7 7-7 7" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
<x-client-pagination x-show="filteredEnvironments.length > 0" class="mt-3 rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]"
|
||||
summary="`${rangeStart}-${rangeEnd} of ${filteredEnvironments.length}`" page-size-model="pageSize"
|
||||
storage-key="coolify.page-size.project-environments" :options="[12, 24, 48, 96]" />
|
||||
</div>
|
||||
|
||||
<div x-show="viewMode === 'table'"
|
||||
@@ -239,31 +216,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<footer x-show="totalPages > 1"
|
||||
class="flex min-h-11 items-center justify-between border-t border-neutral-200 px-4 text-[11px] text-neutral-500 dark:border-white/[0.08] dark:text-fg-faint">
|
||||
<span x-text="`${rangeStart}-${rangeEnd} of ${filteredEnvironments.length}`"></span>
|
||||
<div class="flex items-center gap-1">
|
||||
<button type="button" x-on:click="previousPage" :disabled="page === 1"
|
||||
class="flex size-7 items-center justify-center rounded-md border border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black disabled:pointer-events-none disabled:opacity-35 dark:border-white/[0.08] dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
aria-label="Previous page">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none"
|
||||
aria-hidden="true">
|
||||
<path d="m15 5-7 7 7 7" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" x-on:click="nextPage" :disabled="page >= totalPages"
|
||||
class="flex size-7 items-center justify-center rounded-md border border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black disabled:pointer-events-none disabled:opacity-35 dark:border-white/[0.08] dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
aria-label="Next page">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none"
|
||||
aria-hidden="true">
|
||||
<path d="m9 5 7 7-7 7" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
<x-client-pagination x-show="filteredEnvironments.length > 0"
|
||||
summary="`${rangeStart}-${rangeEnd} of ${filteredEnvironments.length}`" page-size-model="pageSize"
|
||||
storage-key="coolify.page-size.project-environments" :options="[12, 24, 48, 96]" />
|
||||
</div>
|
||||
|
||||
<div x-show="filteredEnvironments.length === 0"
|
||||
|
||||
@@ -253,53 +253,11 @@
|
||||
description="Try a different description or permission." />
|
||||
</div>
|
||||
|
||||
<div x-cloak x-show="filteredTokens.length > 0"
|
||||
class="flex min-h-14 items-center justify-between gap-3 border-t border-neutral-200 px-4 py-3 dark:border-white/[0.08]">
|
||||
<p class="text-[13px] text-neutral-500 dark:text-fg-dim">
|
||||
Showing
|
||||
<span class="tabular-nums text-black dark:text-fg"
|
||||
x-text="`${firstVisibleRow}–${lastVisibleRow}`"></span>
|
||||
of
|
||||
<span class="tabular-nums text-black dark:text-fg"
|
||||
x-text="filteredTokens.length"></span>
|
||||
</p>
|
||||
<div
|
||||
class="inline-flex h-8 overflow-hidden rounded-lg border border-neutral-200 dark:border-white/[0.10]">
|
||||
<button type="button"
|
||||
class="flex w-10 items-center justify-center border-r border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 disabled:cursor-not-allowed disabled:opacity-35 dark:border-white/[0.10] dark:text-fg-dim dark:hover:bg-white/[0.06]"
|
||||
aria-label="First page" title="First page" x-on:click="goToPage(1)"
|
||||
x-bind:disabled="page === 1">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M18 6L12 12L18 18M11 6L5 12L11 18" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="flex w-10 items-center justify-center border-r border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 disabled:cursor-not-allowed disabled:opacity-35 dark:border-white/[0.10] dark:text-fg-dim dark:hover:bg-white/[0.06]"
|
||||
aria-label="Previous page" title="Previous page"
|
||||
x-on:click="goToPage(page - 1)" x-bind:disabled="page === 1">
|
||||
<x-reicon name="arrow-right" class="size-3.5 rotate-180" />
|
||||
</button>
|
||||
<span
|
||||
class="flex min-w-10 items-center justify-center border-r border-neutral-200 px-3 text-[12px] font-medium tabular-nums text-black dark:border-white/[0.10] dark:text-fg"
|
||||
x-text="page"></span>
|
||||
<button type="button"
|
||||
class="flex w-10 items-center justify-center border-r border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 disabled:cursor-not-allowed disabled:opacity-35 dark:border-white/[0.10] dark:text-fg-dim dark:hover:bg-white/[0.06]"
|
||||
aria-label="Next page" title="Next page" x-on:click="goToPage(page + 1)"
|
||||
x-bind:disabled="page === lastPage">
|
||||
<x-reicon name="arrow-right" class="size-3.5" />
|
||||
</button>
|
||||
<button type="button"
|
||||
class="flex w-10 items-center justify-center text-neutral-500 transition-colors hover:bg-neutral-100 disabled:cursor-not-allowed disabled:opacity-35 dark:text-fg-dim dark:hover:bg-white/[0.06]"
|
||||
aria-label="Last page" title="Last page" x-on:click="goToPage(lastPage)"
|
||||
x-bind:disabled="page === lastPage">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M6 6L12 12L6 18M13 6L19 12L13 18" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<x-client-pagination x-cloak x-show="filteredTokens.length > 0"
|
||||
summary="`${firstVisibleRow}-${lastVisibleRow} of ${filteredTokens.length}`"
|
||||
page-size-model="perPage" storage-key="coolify.page-size.api-tokens"
|
||||
previous-action="goToPage(page - 1)" next-action="goToPage(page + 1)"
|
||||
next-disabled="page >= lastPage" />
|
||||
@endif
|
||||
</div>
|
||||
</x-application.settings-section>
|
||||
|
||||
@@ -56,14 +56,11 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative" @click.outside="filterOpen = false"
|
||||
@keydown.escape.window="filterOpen = false">
|
||||
<button type="button" class="button" @click="filterOpen = !filterOpen">
|
||||
<x-table.dropdown panel-class="w-52!">
|
||||
<x-slot:trigger><button type="button" class="button" aria-haspopup="listbox" :aria-expanded="open">
|
||||
<x-reicon name="filter" class="size-3.5" />
|
||||
Filter
|
||||
</button>
|
||||
<div x-show="filterOpen" x-cloak x-transition.opacity.duration.120ms
|
||||
class="listbox-panel left-auto! right-0! z-[90]! min-w-52!">
|
||||
</button></x-slot:trigger>
|
||||
<div
|
||||
class="px-2 py-1 text-[10px] font-semibold tracking-wide text-neutral-400 uppercase dark:text-fg-faint">
|
||||
Type
|
||||
@@ -71,7 +68,7 @@
|
||||
@foreach (['all' => 'All types', 'backup' => 'Backups', 'task' => 'Tasks', 'cleanup' => 'Docker cleanup'] as $value => $label)
|
||||
<button type="button" class="listbox-option"
|
||||
wire:click="$set('filterType', '{{ $value }}')"
|
||||
@click="filterOpen = false">
|
||||
@click="close()">
|
||||
<span>{{ $label }}</span>
|
||||
@if ($filterType === $value)
|
||||
<span class="text-accent">✓</span>
|
||||
@@ -85,36 +82,31 @@
|
||||
@foreach (['last_24h' => 'Last 24 hours', 'last_7d' => 'Last 7 days', 'last_30d' => 'Last 30 days', 'all' => 'All time'] as $value => $label)
|
||||
<button type="button" class="listbox-option"
|
||||
wire:click="$set('filterDate', '{{ $value }}')"
|
||||
@click="filterOpen = false">
|
||||
@click="close()">
|
||||
<span>{{ $label }}</span>
|
||||
@if ($filterDate === $value)
|
||||
<span class="text-accent">✓</span>
|
||||
@endif
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</x-table.dropdown>
|
||||
|
||||
<div class="relative" @click.outside="sortOpen = false"
|
||||
@keydown.escape.window="sortOpen = false">
|
||||
<button type="button" class="button" @click="sortOpen = !sortOpen">
|
||||
<x-table.dropdown panel-class="w-44!">
|
||||
<x-slot:trigger><button type="button" class="button" aria-haspopup="listbox" :aria-expanded="open">
|
||||
<x-reicon name="sort-direction" class="size-3.5" />
|
||||
Sort
|
||||
</button>
|
||||
<div x-show="sortOpen" x-cloak x-transition.opacity.duration.120ms
|
||||
class="listbox-panel left-auto! right-0! z-[90]! min-w-44!">
|
||||
</button></x-slot:trigger>
|
||||
@foreach (['newest' => 'Newest first', 'oldest' => 'Oldest first'] as $value => $label)
|
||||
<button type="button" class="listbox-option"
|
||||
wire:click="$set('sortOrder', '{{ $value }}')"
|
||||
@click="sortOpen = false">
|
||||
@click="close()">
|
||||
<span>{{ $label }}</span>
|
||||
@if ($sortOrder === $value)
|
||||
<span class="text-accent">✓</span>
|
||||
@endif
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</x-table.dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -191,26 +183,11 @@
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div
|
||||
class="flex min-h-12 items-center justify-between gap-3 border-t border-neutral-200 px-4 dark:border-white/[0.08]">
|
||||
<span class="whitespace-nowrap text-[11px] text-neutral-500 dark:text-fg-faint">
|
||||
Showing <span x-text="`${firstVisibleRow}–${lastVisibleRow}`"></span> of
|
||||
{{ $executions->count() }}
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[11px] text-neutral-500 dark:text-fg-dim">
|
||||
Page <span x-text="page"></span> of <span x-text="lastPage"></span>
|
||||
</span>
|
||||
<button type="button" class="button size-8! px-0!" x-on:click="goToPage(page - 1)"
|
||||
x-bind:disabled="page === 1" aria-label="Previous page">
|
||||
<x-reicon name="arrow-right" class="size-3.5 rotate-180" />
|
||||
</button>
|
||||
<button type="button" class="button size-8! px-0!" x-on:click="goToPage(page + 1)"
|
||||
x-bind:disabled="page === lastPage" aria-label="Next page">
|
||||
<x-reicon name="arrow-right" class="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<x-client-pagination
|
||||
summary="`${firstVisibleRow}-${lastVisibleRow} of ${total}`"
|
||||
page-size-model="perPage" storage-key="coolify.page-size.scheduled-jobs"
|
||||
previous-action="goToPage(page - 1)" next-action="goToPage(page + 1)"
|
||||
next-disabled="page >= lastPage" />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -260,26 +237,11 @@
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div
|
||||
class="flex min-h-12 items-center justify-between gap-3 border-t border-neutral-200 px-4 dark:border-white/[0.08]">
|
||||
<span class="whitespace-nowrap text-[11px] text-neutral-500 dark:text-fg-faint">
|
||||
Showing <span x-text="`${firstVisibleRow}–${lastVisibleRow}`"></span> of
|
||||
{{ $managerRuns->count() }}
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[11px] text-neutral-500 dark:text-fg-dim">
|
||||
Page <span x-text="page"></span> of <span x-text="lastPage"></span>
|
||||
</span>
|
||||
<button type="button" class="button size-8! px-0!" x-on:click="goToPage(page - 1)"
|
||||
x-bind:disabled="page === 1" aria-label="Previous page">
|
||||
<x-reicon name="arrow-right" class="size-3.5 rotate-180" />
|
||||
</button>
|
||||
<button type="button" class="button size-8! px-0!" x-on:click="goToPage(page + 1)"
|
||||
x-bind:disabled="page === lastPage" aria-label="Next page">
|
||||
<x-reicon name="arrow-right" class="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<x-client-pagination
|
||||
summary="`${firstVisibleRow}-${lastVisibleRow} of ${total}`"
|
||||
page-size-model="perPage" storage-key="coolify.page-size.scheduled-jobs"
|
||||
previous-action="goToPage(page - 1)" next-action="goToPage(page + 1)"
|
||||
next-disabled="page >= lastPage" />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -337,27 +299,12 @@
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div
|
||||
class="flex min-h-12 items-center justify-between gap-3 border-t border-neutral-200 px-4 dark:border-white/[0.08]">
|
||||
<span class="whitespace-nowrap text-[11px] text-neutral-500 dark:text-fg-faint">
|
||||
{{ $skipTotalCount }} {{ Str::plural('skipped job', $skipTotalCount) }}
|
||||
</span>
|
||||
@if ($skipTotalCount > $skipDefaultTake)
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[11px] text-neutral-500 dark:text-fg-dim">
|
||||
Page {{ $skipCurrentPage }} of {{ ceil($skipTotalCount / $skipDefaultTake) }}
|
||||
</span>
|
||||
<button type="button" class="button size-8! px-0!" wire:click="skipPreviousPage"
|
||||
@disabled(!$showSkipPrev) aria-label="Previous page">
|
||||
<x-reicon name="arrow-right" class="size-3.5 rotate-180" />
|
||||
</button>
|
||||
<button type="button" class="button size-8! px-0!" wire:click="skipNextPage"
|
||||
@disabled(!$showSkipNext) aria-label="Next page">
|
||||
<x-reicon name="arrow-right" class="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<x-table-pagination :from="$skipTotalCount === 0 ? 0 : $skipPage + 1"
|
||||
:to="min($skipPage + $skipLogs->count(), $skipTotalCount)" :total="$skipTotalCount"
|
||||
:current-page="$skipCurrentPage"
|
||||
:last-page="max(1, (int) ceil($skipTotalCount / $skipDefaultTake))"
|
||||
wire-target="skipPreviousPage,skipNextPage" previous-action="skipPreviousPage"
|
||||
next-action="skipNextPage" />
|
||||
@endif
|
||||
</div>
|
||||
</x-application.settings-section>
|
||||
|
||||
@@ -42,8 +42,9 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative" x-on:click.outside="sortOpen = false">
|
||||
<button type="button" class="button" x-on:click="sortOpen = !sortOpen">
|
||||
<x-table.dropdown panel-class="w-52!">
|
||||
<x-slot:trigger>
|
||||
<button type="button" class="button" aria-haspopup="listbox" :aria-expanded="open">
|
||||
<svg class="size-3.5 opacity-65" viewBox="0 0 24 24" fill="none"
|
||||
aria-hidden="true">
|
||||
<path d="M8 5v14m0 0-3-3m3 3 3-3M16 19V5m0 0-3 3m3-3 3 3"
|
||||
@@ -51,13 +52,12 @@
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
Sort
|
||||
</button>
|
||||
<div x-cloak x-show="sortOpen" x-transition.origin.top.right
|
||||
class="absolute top-9 right-0 z-50 w-52 rounded-lg border border-neutral-200 bg-white p-1 shadow-modal dark:border-white/[0.1] dark:bg-raised">
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
<template x-for="option in sortOptions" :key="option.value">
|
||||
<button type="button"
|
||||
class="flex h-9 w-full items-center rounded-md px-2 text-left text-[12px] text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
x-on:click="sortBy = option.value; sortOpen = false; page = 1">
|
||||
x-on:click="sortBy = option.value; close(); page = 1">
|
||||
<span class="flex-1" x-text="option.label"></span>
|
||||
<svg x-show="sortBy === option.value" class="size-3.5 text-warning"
|
||||
viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
@@ -66,8 +66,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</x-table.dropdown>
|
||||
|
||||
<div
|
||||
class="flex h-9 items-center rounded-lg border border-neutral-200 bg-white p-0.5 dark:border-white/[0.08] dark:bg-white/[0.035]">
|
||||
@@ -122,29 +121,9 @@
|
||||
</article>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<footer x-show="totalPages > 1"
|
||||
class="mt-3 flex min-h-11 items-center justify-between rounded-xl border border-neutral-200 bg-white px-4 text-[11px] text-neutral-500 shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
<span x-text="`${rangeStart}-${rangeEnd} of ${filteredTags.length}`"></span>
|
||||
<div class="flex items-center gap-1">
|
||||
<button type="button" x-on:click="previousPage" :disabled="page === 1"
|
||||
class="flex size-7 items-center justify-center rounded-md border border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black disabled:pointer-events-none disabled:opacity-35 dark:border-white/[0.08] dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
aria-label="Previous page">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="m15 5-7 7 7 7" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" x-on:click="nextPage" :disabled="page >= totalPages"
|
||||
class="flex size-7 items-center justify-center rounded-md border border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black disabled:pointer-events-none disabled:opacity-35 dark:border-white/[0.08] dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
aria-label="Next page">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="m9 5 7 7-7 7" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
<x-client-pagination x-show="filteredTags.length > 0" class="mt-3 rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]"
|
||||
summary="`${rangeStart}-${rangeEnd} of ${filteredTags.length}`" page-size-model="pageSize"
|
||||
storage-key="coolify.page-size.tags" :options="[12, 24, 48, 96]" />
|
||||
</div>
|
||||
|
||||
<div x-show="viewMode === 'table'"
|
||||
@@ -176,29 +155,9 @@
|
||||
x-text="tag.servicesCount"></div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<footer x-show="totalPages > 1"
|
||||
class="flex min-h-11 items-center justify-between border-t border-neutral-200 px-4 text-[11px] text-neutral-500 dark:border-white/[0.08] dark:text-fg-faint">
|
||||
<span x-text="`${rangeStart}-${rangeEnd} of ${filteredTags.length}`"></span>
|
||||
<div class="flex items-center gap-1">
|
||||
<button type="button" x-on:click="previousPage" :disabled="page === 1"
|
||||
class="flex size-7 items-center justify-center rounded-md border border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black disabled:pointer-events-none disabled:opacity-35 dark:border-white/[0.08] dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
aria-label="Previous page">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="m15 5-7 7 7 7" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" x-on:click="nextPage" :disabled="page >= totalPages"
|
||||
class="flex size-7 items-center justify-center rounded-md border border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-black disabled:pointer-events-none disabled:opacity-35 dark:border-white/[0.08] dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
aria-label="Next page">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="m9 5 7 7-7 7" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
<x-client-pagination x-show="filteredTags.length > 0"
|
||||
summary="`${rangeStart}-${rangeEnd} of ${filteredTags.length}`" page-size-model="pageSize"
|
||||
storage-key="coolify.page-size.tags" :options="[12, 24, 48, 96]" />
|
||||
</div>
|
||||
|
||||
<div x-show="filteredTags.length === 0"
|
||||
|
||||
@@ -89,7 +89,11 @@
|
||||
:current-page="$users->currentPage()" :last-page="$users->lastPage()"
|
||||
wire-target="setPage,previousPage,nextPage" first-action="setPage(1)"
|
||||
previous-action="previousPage" next-action="nextPage"
|
||||
last-action="setPage({{ $users->lastPage() }})" />
|
||||
last-action="setPage({{ $users->lastPage() }})">
|
||||
<x-slot:pageSize>
|
||||
<x-page-size-select model="perPage" livewire storage-key="coolify.page-size.team-admin" />
|
||||
</x-slot:pageSize>
|
||||
</x-table-pagination>
|
||||
@else
|
||||
<x-empty title="No users found" description="Try a different name or email address."
|
||||
icon-name="teams" size="sm" />
|
||||
|
||||
@@ -129,58 +129,11 @@
|
||||
description="Try a different name, email address, or role." />
|
||||
</div>
|
||||
|
||||
<div x-cloak x-show="filteredMembers.length > 0"
|
||||
class="flex min-h-14 items-center justify-between gap-3 border-t border-neutral-200 px-4 py-3 dark:border-white/[0.08]">
|
||||
<p class="text-[13px] text-neutral-500 dark:text-fg-dim">
|
||||
Showing
|
||||
<span class="tabular-nums text-black dark:text-fg"
|
||||
x-text="`${firstVisibleRow}–${lastVisibleRow}`"></span>
|
||||
of
|
||||
<span class="tabular-nums text-black dark:text-fg" x-text="filteredMembers.length"></span>
|
||||
</p>
|
||||
<div
|
||||
class="inline-flex h-8 overflow-hidden rounded-lg border border-neutral-200 dark:border-white/[0.10]">
|
||||
<button type="button"
|
||||
class="flex w-10 items-center justify-center border-r border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 disabled:cursor-not-allowed disabled:opacity-35 dark:border-white/[0.10] dark:text-fg-dim dark:hover:bg-white/[0.06]"
|
||||
aria-label="First page" title="First page" x-on:click="goToPage(1)"
|
||||
x-bind:disabled="page === 1">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M18 6L12 12L18 18M11 6L5 12L11 18" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="flex w-10 items-center justify-center border-r border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 disabled:cursor-not-allowed disabled:opacity-35 dark:border-white/[0.10] dark:text-fg-dim dark:hover:bg-white/[0.06]"
|
||||
aria-label="Previous page" title="Previous page" x-on:click="goToPage(page - 1)"
|
||||
x-bind:disabled="page === 1">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M15 6L9 12L15 18" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<span
|
||||
class="flex min-w-12 items-center justify-center border-r border-neutral-200 px-3 text-[13px] tabular-nums text-black dark:border-white/[0.10] dark:text-fg"
|
||||
x-text="page"></span>
|
||||
<button type="button"
|
||||
class="flex w-10 items-center justify-center border-r border-neutral-200 text-neutral-500 transition-colors hover:bg-neutral-100 disabled:cursor-not-allowed disabled:opacity-35 dark:border-white/[0.10] dark:text-fg-dim dark:hover:bg-white/[0.06]"
|
||||
aria-label="Next page" title="Next page" x-on:click="goToPage(page + 1)"
|
||||
x-bind:disabled="page === lastPage">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M9 6L15 12L9 18" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="flex w-10 items-center justify-center text-neutral-500 transition-colors hover:bg-neutral-100 disabled:cursor-not-allowed disabled:opacity-35 dark:text-fg-dim dark:hover:bg-white/[0.06]"
|
||||
aria-label="Last page" title="Last page" x-on:click="goToPage(lastPage)"
|
||||
x-bind:disabled="page === lastPage">
|
||||
<svg class="size-3.5" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M6 6L12 12L6 18M13 6L19 12L13 18" stroke="currentColor"
|
||||
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<x-client-pagination x-cloak x-show="filteredMembers.length > 0"
|
||||
summary="`${firstVisibleRow}-${lastVisibleRow} of ${filteredMembers.length}`"
|
||||
page-size-model="perPage" storage-key="coolify.page-size.team-members"
|
||||
previous-action="goToPage(page - 1)" next-action="goToPage(page + 1)"
|
||||
next-disabled="page >= lastPage" />
|
||||
</x-application.settings-section>
|
||||
|
||||
@can('manageInvitations', currentTeam())
|
||||
|
||||
@@ -39,6 +39,29 @@ test('listbox component uses shared trigger label truncation', function () {
|
||||
->not->toContain('class="truncate" x-text="current"');
|
||||
});
|
||||
|
||||
test('portaled listboxes center in the mobile viewport', function () {
|
||||
$html = Blade::render('<x-forms.listbox id="region" :options="[]" :wire="false" />');
|
||||
|
||||
expect($html)
|
||||
->toContain('window.innerWidth < 768')
|
||||
->toContain('(window.innerWidth - panelWidth) / 2')
|
||||
->toContain('panel.style.width = `${panelWidth}px`')
|
||||
->toContain('panel.style.maxWidth = `${window.innerWidth - (edge * 2)}px`')
|
||||
->toContain('x-show="open"')
|
||||
->not->toContain('x-show="open && positioned"');
|
||||
});
|
||||
|
||||
test('legacy floating panels have a centered mobile fallback', function () {
|
||||
$css = file_get_contents(resource_path('css/app.css'));
|
||||
|
||||
expect($css)
|
||||
->toContain('@media (max-width: 767px)')
|
||||
->toContain('.listbox-panel:not([style*="position: fixed"])')
|
||||
->toContain('position: fixed !important;')
|
||||
->toContain('left: 50% !important;')
|
||||
->toContain('transform: translate(-50%, -50%) !important;');
|
||||
});
|
||||
|
||||
test('searchable listbox component uses shared trigger label truncation', function () {
|
||||
$html = Blade::render(<<<'BLADE'
|
||||
<x-forms.searchable-listbox id="tz" label="Timezone"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
it('renders the standard table toolbar controls', function () {
|
||||
$html = Blade::render(<<<'BLADE'
|
||||
@@ -21,6 +22,16 @@ it('renders the standard table toolbar controls', function () {
|
||||
->toContain('table-toolbar')
|
||||
->toContain('table-search')
|
||||
->toContain('wire:model.live="search"')
|
||||
->toContain('x-teleport="body"')
|
||||
->toContain('positionPanel')
|
||||
->toContain('position: fixed')
|
||||
->toContain('floating-dropdown-panel')
|
||||
->toContain('window.innerWidth < 768')
|
||||
->toContain('(window.innerWidth - panelWidth) / 2')
|
||||
->toContain('panel.style.width = `${panelWidth}px`')
|
||||
->toContain('panel.style.maxWidth = `${window.innerWidth - (edge * 2)}px`')
|
||||
->toContain('x-show="open"')
|
||||
->not->toContain('x-show="open && positioned"')
|
||||
->toContain('aria-multiselectable="true"')
|
||||
->toContain('Reset filters')
|
||||
->toContain('wire:click="clearFilters"')
|
||||
@@ -61,3 +72,30 @@ it('uses the standard search control for frontend filtered infrastructure tables
|
||||
->toContain('clear-when="search"')
|
||||
->toContain("clear-action=\"search = ''\"");
|
||||
});
|
||||
|
||||
it('uses the collision aware dropdown on the projects page', function () {
|
||||
$projects = file_get_contents(resource_path('views/livewire/project/index.blade.php'));
|
||||
|
||||
expect($projects)
|
||||
->toContain('<x-table.dropdown')
|
||||
->not->toContain('x-show="sortOpen"');
|
||||
});
|
||||
|
||||
it('uses collision aware filter and sort dropdowns on the resources page', function () {
|
||||
$resources = file_get_contents(resource_path('views/livewire/project/resource/index.blade.php'));
|
||||
|
||||
expect(substr_count($resources, '<x-table.dropdown'))->toBeGreaterThanOrEqual(2)
|
||||
->and($resources)
|
||||
->not->toContain('x-show="filterOpen"')
|
||||
->not->toContain('x-show="sortOpen"');
|
||||
});
|
||||
|
||||
it('does not use legacy floating sort or filter panels', function () {
|
||||
$views = collect(File::allFiles(resource_path('views')))
|
||||
->filter(fn (SplFileInfo $file): bool => $file->getExtension() === 'php')
|
||||
->map(fn (SplFileInfo $file): string => $file->getContents())
|
||||
->implode("\n");
|
||||
|
||||
expect($views)
|
||||
->not->toMatch('/x-show="(?:sortOpen|filterOpen|filtersOpen)"/');
|
||||
});
|
||||
@@ -2,6 +2,47 @@
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
|
||||
it('renders the shared page size selector', function () {
|
||||
$html = Blade::render('<x-page-size-select model="perPage" livewire storage-key="tests.page-size" />');
|
||||
|
||||
expect($html)
|
||||
->toContain('aria-label="Items per page"')
|
||||
->toContain("\$wire.set('perPage'")
|
||||
->toContain('h-7!')
|
||||
->toContain('w-12!')
|
||||
->toContain('opacity-0')
|
||||
->toContain('selectedPageSize')
|
||||
->toContain('border-0')
|
||||
->toContain('text-[11px]!')
|
||||
->toContain('mb-0!')
|
||||
->toContain("localStorage.getItem('tests.page-size')")
|
||||
->toContain("localStorage.setItem('tests.page-size'")
|
||||
->toContain('value="10"')
|
||||
->toContain('value="25"')
|
||||
->toContain('value="50"')
|
||||
->toContain('value="100"');
|
||||
expect($html)
|
||||
->toContain('value="custom"')
|
||||
->toContain('data-custom-page-size')
|
||||
->toContain('customOption?.remove()')
|
||||
->not->toContain('<option value="" disabled selected>')
|
||||
->toContain('min="1"')
|
||||
->toContain('max="100"');
|
||||
});
|
||||
|
||||
it('renders compact client-side pagination', function () {
|
||||
$html = Blade::render('<x-client-pagination summary="1-10 of 20" page-size-model="pageSize" storage-key="tests.page-size" />');
|
||||
|
||||
expect($html)
|
||||
->toContain('1-10 of 20')
|
||||
->toContain('pageSize = pageSizeValue')
|
||||
->toContain("localStorage.getItem('tests.page-size')")
|
||||
->toContain('aria-label="Previous page"')
|
||||
->toContain('aria-label="Next page"')
|
||||
->not->toContain('aria-label="First page"')
|
||||
->not->toContain('aria-label="Last page"');
|
||||
});
|
||||
|
||||
it('renders loading indicators for livewire page navigation', function () {
|
||||
$html = Blade::render(
|
||||
'<x-table-pagination
|
||||
@@ -19,24 +60,21 @@ it('renders loading indicators for livewire page navigation', function () {
|
||||
);
|
||||
|
||||
expect($html)
|
||||
->toContain('Showing')
|
||||
->toContain('1–10')
|
||||
->toContain('13')
|
||||
->toContain('wire:target="goToPage,previousPage,nextPage"')
|
||||
->toContain('wire:loading')
|
||||
->toContain('wire:loading.remove')
|
||||
->toContain('wire:loading.attr="disabled"')
|
||||
->toContain('wire:loading.inline-flex')
|
||||
->toContain('animate-spin')
|
||||
->toContain('Loading page…')
|
||||
->toContain('wire:click="goToPage(1)"')
|
||||
->toContain('wire:click="previousPage"')
|
||||
->toContain('wire:click="nextPage"')
|
||||
->toContain('wire:click="goToPage(2)"')
|
||||
->toContain('aria-label="First page"')
|
||||
->toContain('aria-label="Next page"');
|
||||
->toContain('aria-label="Next page"')
|
||||
->not->toContain('aria-label="First page"')
|
||||
->not->toContain('aria-label="Last page"');
|
||||
|
||||
// Single spinner only (page number slot), not duplicated next to the "Showing" label.
|
||||
// A single spinner appears next to the range while Livewire loads the next page.
|
||||
expect(substr_count($html, 'animate-spin'))->toBe(1)
|
||||
->and(substr_count($html, 'Loading page…'))->toBe(1);
|
||||
});
|
||||
@@ -53,7 +91,6 @@ it('omits livewire loading markup when no wire target is provided', function ()
|
||||
);
|
||||
|
||||
expect($html)
|
||||
->toContain('Showing')
|
||||
->toContain('1–5')
|
||||
->not->toContain('wire:loading')
|
||||
->not->toContain('wire:target')
|
||||
@@ -65,9 +102,10 @@ it('uses the shared pagination loading component on deployment history', functio
|
||||
|
||||
expect($view)
|
||||
->toContain('<x-table-pagination')
|
||||
->toContain('<x-page-size-select')
|
||||
->toContain('wire-target="goToPage,previousPage,nextPage"')
|
||||
->toContain('wire:loading.class="opacity-50 pointer-events-none"')
|
||||
->toContain('wire:target="goToPage,previousPage,nextPage"');
|
||||
->toContain('wire:target="goToPage,previousPage,nextPage,toggleDeploymentFilter,clearFilter,setPullRequestFilter"');
|
||||
});
|
||||
|
||||
it('uses the shared pagination loading component on environment variables', function () {
|
||||
@@ -75,8 +113,9 @@ it('uses the shared pagination loading component on environment variables', func
|
||||
|
||||
expect($view)
|
||||
->toContain('<x-table-pagination')
|
||||
->toContain('<x-page-size-select')
|
||||
->toContain('wire-target="setEnvironmentVariablePage,previousEnvironmentVariablePage,nextEnvironmentVariablePage"')
|
||||
->toContain('wire:loading.class="opacity-50 pointer-events-none"');
|
||||
->toContain('wire:loading.class="pointer-events-none opacity-40 blur-[2px]"');
|
||||
});
|
||||
|
||||
it('uses the shared pagination loading component on team admin view', function () {
|
||||
@@ -84,5 +123,37 @@ it('uses the shared pagination loading component on team admin view', function (
|
||||
|
||||
expect($view)
|
||||
->toContain('<x-table-pagination')
|
||||
->toContain('<x-page-size-select')
|
||||
->toContain('wire-target="setPage,previousPage,nextPage"');
|
||||
});
|
||||
|
||||
it('offers page size selection on client-side paginated tables', function (string $view) {
|
||||
$contents = file_get_contents(resource_path("views/{$view}"));
|
||||
|
||||
expect($contents)
|
||||
->toContain('storage-key="coolify.page-size.')
|
||||
->toMatch('/<x-(page-size-select|client-pagination)/');
|
||||
})->with([
|
||||
'team members' => 'livewire/team/member/index.blade.php',
|
||||
'api tokens' => 'livewire/security/api-tokens.blade.php',
|
||||
'scheduled executions' => 'livewire/settings/scheduled-jobs.blade.php',
|
||||
'volume backup executions' => 'livewire/project/shared/storages/volume-backups/executions.blade.php',
|
||||
'environment resources' => 'livewire/project/resource/index.blade.php',
|
||||
'projects' => 'livewire/project/index.blade.php',
|
||||
'project environments' => 'livewire/project/show.blade.php',
|
||||
'tagged resources' => 'livewire/tags/show.blade.php',
|
||||
]);
|
||||
|
||||
it('uses compact client pagination on collection views', function (string $view) {
|
||||
$contents = file_get_contents(resource_path("views/{$view}"));
|
||||
|
||||
expect($contents)->toContain('<x-client-pagination');
|
||||
})->with([
|
||||
'team members' => 'livewire/team/member/index.blade.php',
|
||||
'api tokens' => 'livewire/security/api-tokens.blade.php',
|
||||
'scheduled jobs' => 'livewire/settings/scheduled-jobs.blade.php',
|
||||
'environment resources' => 'livewire/project/resource/index.blade.php',
|
||||
'projects' => 'livewire/project/index.blade.php',
|
||||
'project environments' => 'livewire/project/show.blade.php',
|
||||
'tags' => 'livewire/tags/show.blade.php',
|
||||
]);
|
||||
Reference in new issue
Block a user