New Blade layout, copy, and visual system for the monitoring tab; RRD hosts use data-statistics-chart hooks; tables use pxStatTableKit and data-statistics-data-row; network and comparison panels use new selectors; CSV helpers renamed (pxExport*); modals and telemetry refreshed. Made-with: Cursor
434 lines
32 KiB
PHP
434 lines
32 KiB
PHP
<!-- Statistics History Table -->
|
|
@php
|
|
$statisticsExportRows = [];
|
|
@endphp
|
|
<div class="mt-10 ring-1 ring-neutral/40 rounded-2xl overflow-hidden bg-background-secondary/80 shadow-sm" data-statistics-panel="samples-archive" id="px-stat-samples-card">
|
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between p-5 border-b border-neutral/50 bg-gradient-to-br from-indigo-500/12 via-transparent to-transparent">
|
|
<div class="flex items-start gap-3 min-w-0 flex-1">
|
|
<div class="flex items-center justify-center w-11 h-11 rounded-2xl bg-indigo-500/15 shrink-0 ring-1 ring-indigo-500/20">
|
|
<x-ri-database-2-line class="size-5 text-indigo-500" />
|
|
</div>
|
|
<div class="min-w-0">
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<h3 class="text-base font-semibold tracking-tight">Archived guest samples</h3>
|
|
<div class="relative" x-data="{ open: false }" x-cloak>
|
|
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors">
|
|
<x-ri-question-line class="size-4" />
|
|
</button>
|
|
<!-- Modal Backdrop -->
|
|
<div x-show="open"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="opacity-100"
|
|
x-transition:leave-end="opacity-0"
|
|
@click="open = false"
|
|
@keydown.escape.window="open = false"
|
|
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
|
|
style="display: none;">
|
|
</div>
|
|
<!-- Modal -->
|
|
<div x-show="open"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="opacity-100 scale-100"
|
|
x-transition:leave-end="opacity-0 scale-95"
|
|
@click.self="open = false"
|
|
@keydown.escape.window="open = false"
|
|
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
|
style="display: none;">
|
|
<div class="relative w-full max-w-2xl max-h-[90vh] overflow-y-auto bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
|
|
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
<div class="flex items-center gap-3 mb-4 pr-8">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-indigo-500/15">
|
|
<x-ri-database-2-line class="size-5 text-indigo-500" />
|
|
</div>
|
|
<h6 class="font-sans text-lg font-semibold leading-tight">
|
|
How archives work
|
|
</h6>
|
|
</div>
|
|
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
|
|
Each row is a frozen reading of CPU, RAM, disks, and network counters. Use it to audit drift, correlate incidents, or prove capacity before upgrades.
|
|
</p>
|
|
<div class="pt-4 mt-4 border-t border-neutral/30 space-y-2.5">
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">Rolling automation:</span> The platform captures hourly samples for every VPS and trims the backlog at 24 automatic rows.
|
|
</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">Manual pins:</span> You may stash up to five on-demand snapshots beside the automated series and drop them individually.
|
|
</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">Reading the grid:</span> CPU/RAM/Disk are percentages. Network columns show deltas versus the prior row; disk I/O pairs read/write volume; uptime counts guest runtime.
|
|
</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">Removing rows:</span> The trash control on each line purges that sample; clearing everything opens the dedicated confirmation modal.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2 shrink-0 flex-wrap"
|
|
x-data
|
|
@statistics-export:no-data.window="$wire.showExportInfo($event.detail.message)">
|
|
<x-button.primary wire:click="createLiveStatistic" class="whitespace-nowrap">
|
|
<x-loading target="createLiveStatistic" />
|
|
<x-ri-refresh-line class="size-4" />
|
|
<div wire:loading.remove wire:target="createLiveStatistic">
|
|
Capture now
|
|
</div>
|
|
<div wire:loading wire:target="createLiveStatistic">
|
|
Saving…
|
|
</div>
|
|
</x-button.primary>
|
|
<button type="button"
|
|
wire:click="openClearStatisticsModal"
|
|
wire:loading.attr="disabled"
|
|
wire:target="clearStatisticsHistory"
|
|
class="bg-danger/10 border border-danger/30 text-danger px-3 py-2 rounded-md text-sm font-semibold hover:bg-danger/20 flex items-center gap-2 h-[calc(0.5rem*2+1.25rem)]"
|
|
title="Remove every archived sample">
|
|
<x-ri-delete-bin-line class="size-4" />
|
|
<span wire:loading.remove wire:target="clearStatisticsHistory">Purge all</span>
|
|
<span wire:loading wire:target="clearStatisticsHistory">Purging…</span>
|
|
</button>
|
|
<button type="button"
|
|
@click="pxExportSamplesArchiveCsv()"
|
|
class="bg-background border border-neutral px-3 py-2 rounded-md text-sm font-semibold hover:bg-background/80 flex items-center gap-2 h-[calc(0.5rem*2+1.25rem)]"
|
|
title="Download CSV">
|
|
<x-ri-download-line class="size-4" />
|
|
CSV
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div x-data="pxStatTableKit('px-stat-samples')">
|
|
<div class="p-4 border-b border-neutral/40 bg-background/40">
|
|
<div class="flex items-center gap-3 flex-wrap">
|
|
<div class="flex-1 min-w-[200px]">
|
|
<input type="text"
|
|
x-model="search"
|
|
@input="filterTable()"
|
|
placeholder="Filter by date, state, or metric…"
|
|
class="w-full px-3 py-2 text-sm bg-background border border-neutral rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500/30">
|
|
</div>
|
|
<button @click="search = ''; filterTable()"
|
|
x-show="search"
|
|
class="px-3 py-2 text-sm text-base/70 hover:text-base border border-neutral rounded-lg hover:bg-background/80">
|
|
Reset
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full" id="px-stat-samples">
|
|
<thead>
|
|
<tr class="border-b border-neutral bg-background">
|
|
<th @click.stop="sortTable('date-time')" data-sort="date-time" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
|
|
<div class="flex items-center gap-2">
|
|
Captured
|
|
<x-ri-arrow-up-down-line class="size-3" />
|
|
</div>
|
|
</th>
|
|
<th @click.stop="sortTable('status')" data-sort="status" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
|
|
<div class="flex items-center gap-2">
|
|
Power
|
|
<x-ri-arrow-up-down-line class="size-3" />
|
|
</div>
|
|
</th>
|
|
<th @click.stop="sortTable('cpu')" data-sort="cpu" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
|
|
<div class="flex items-center gap-2">
|
|
CPU
|
|
<x-ri-arrow-up-down-line class="size-3" />
|
|
</div>
|
|
</th>
|
|
<th @click.stop="sortTable('memory')" data-sort="memory" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
|
|
<div class="flex items-center gap-2">
|
|
RAM
|
|
<x-ri-arrow-up-down-line class="size-3" />
|
|
</div>
|
|
</th>
|
|
<th @click.stop="sortTable('disk')" data-sort="disk" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
|
|
<div class="flex items-center gap-2">
|
|
Disk
|
|
<x-ri-arrow-up-down-line class="size-3" />
|
|
</div>
|
|
</th>
|
|
<th @click.stop="sortTable('network-in')" data-sort="network-in" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
|
|
<div class="flex items-center gap-2">
|
|
Net in
|
|
<x-ri-arrow-up-down-line class="size-3" />
|
|
</div>
|
|
</th>
|
|
<th @click.stop="sortTable('network-out')" data-sort="network-out" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
|
|
<div class="flex items-center gap-2">
|
|
Net out
|
|
<x-ri-arrow-up-down-line class="size-3" />
|
|
</div>
|
|
</th>
|
|
<th @click.stop="sortTable('disk-io')" data-sort="disk-io" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
|
|
<div class="flex items-center gap-2">
|
|
Block I/O
|
|
<x-ri-arrow-up-down-line class="size-3" />
|
|
</div>
|
|
</th>
|
|
<th @click.stop="sortTable('uptime')" data-sort="uptime" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
|
|
<div class="flex items-center gap-2">
|
|
Uptime
|
|
<x-ri-arrow-up-down-line class="size-3" />
|
|
</div>
|
|
</th>
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-neutral/40">
|
|
@forelse($this->statistics as $stat)
|
|
@php
|
|
$networkStats = $this->getCurrentNetworkStats($stat);
|
|
$diskStats = $this->getCurrentDiskStats($stat);
|
|
$cpuPercent = $stat->cpumax > 0 ? ($stat->cpuused / $stat->cpumax * 100) : 0;
|
|
$memPercent = $stat->memorymax > 0 ? ($stat->memoryused / $stat->memorymax * 100) : 0;
|
|
$diskPercent = ($stat->diskmax ?? 0) > 0 ? (($stat->diskused ?? 0) / ($stat->diskmax ?? 1) * 100) : 0;
|
|
$statisticsExportRows[] = [
|
|
'date_time' => $stat->DateTime ? $stat->DateTime->format('M d, Y H:i:s') : '-',
|
|
'status' => $stat->vmstatus ? ucfirst($stat->vmstatus) : '-',
|
|
'cpu' => number_format($cpuPercent, 1) . '%',
|
|
'memory' => number_format($memPercent, 1) . '%',
|
|
'disk' => (($stat->diskmax ?? 0) > 0 && ($stat->diskused ?? 0) > 0) ? number_format($diskPercent, 1) . '%' : 'N/A',
|
|
'network_in' => Illuminate\Support\Number::fileSize(bytes: $networkStats['netin']),
|
|
'network_out' => Illuminate\Support\Number::fileSize(bytes: $networkStats['netout']),
|
|
'disk_io' => sprintf(
|
|
'%s / %s',
|
|
Illuminate\Support\Number::fileSize(bytes: $diskStats['diskread'] ?? 0),
|
|
Illuminate\Support\Number::fileSize(bytes: $diskStats['diskwrite'] ?? 0)
|
|
),
|
|
'uptime' => $stat->uptime > 0 ? $this->formatUptime($stat->uptime) : '-',
|
|
];
|
|
@endphp
|
|
<tr wire:key="stat-{{ $stat->id }}"
|
|
data-statistics-data-row
|
|
class="hover:bg-background-secondary/40 transition-colors"
|
|
data-date-time="{{ $stat->DateTime ? $stat->DateTime->format('Y-m-d H:i:s') : '' }}"
|
|
data-status="{{ strtolower($stat->vmstatus ?? '') }}"
|
|
data-cpu="{{ number_format($cpuPercent, 2) }}"
|
|
data-memory="{{ number_format($memPercent, 2) }}"
|
|
data-disk="{{ number_format($diskPercent, 2) }}"
|
|
data-network-in="{{ $networkStats['netin'] }}"
|
|
data-network-out="{{ $networkStats['netout'] }}"
|
|
data-disk-io="{{ ($diskStats['diskread'] ?? 0) + ($diskStats['diskwrite'] ?? 0) }}"
|
|
data-uptime="{{ $stat->uptime ?? 0 }}">
|
|
<td class="py-3 px-4 text-sm whitespace-nowrap">
|
|
<div class="font-medium">{{ $stat->DateTime?->format('M d, Y') ?? '-' }}</div>
|
|
<div class="text-xs text-base/60">{{ $stat->DateTime?->format('H:i:s') ?? '' }}</div>
|
|
</td>
|
|
<td class="py-3 px-4 text-sm whitespace-nowrap">
|
|
@if($stat->vmstatus)
|
|
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-semibold {{ $stat->vmstatus === 'running' ? 'bg-success/20 text-success' : 'bg-inactive/20 text-inactive' }}">
|
|
{{ ucfirst($stat->vmstatus) }}
|
|
</span>
|
|
@else
|
|
<span class="text-base/50">-</span>
|
|
@endif
|
|
</td>
|
|
<td class="py-3 px-4 text-sm whitespace-nowrap">
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-12 bg-background rounded-full h-1.5 overflow-hidden">
|
|
<div class="h-full bg-blue-500 rounded-full" style="width: {{ min($cpuPercent, 100) }}%"></div>
|
|
</div>
|
|
<span class="font-medium">{{ number_format($cpuPercent, 1) }}%</span>
|
|
</div>
|
|
</td>
|
|
<td class="py-3 px-4 text-sm whitespace-nowrap">
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-12 bg-background rounded-full h-1.5 overflow-hidden">
|
|
<div class="h-full bg-green-500 rounded-full" style="width: {{ min($memPercent, 100) }}%"></div>
|
|
</div>
|
|
<span class="font-medium">{{ number_format($memPercent, 1) }}%</span>
|
|
</div>
|
|
</td>
|
|
<td class="py-3 px-4 text-sm whitespace-nowrap">
|
|
@if(($stat->diskmax ?? 0) > 0 && ($stat->diskused ?? 0) > 0)
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-12 bg-background rounded-full h-1.5 overflow-hidden">
|
|
<div class="h-full bg-purple-500 rounded-full" style="width: {{ min($diskPercent, 100) }}%"></div>
|
|
</div>
|
|
<span class="font-medium">{{ number_format($diskPercent, 1) }}%</span>
|
|
</div>
|
|
@else
|
|
<span class="font-medium">N/A</span>
|
|
@endif
|
|
</td>
|
|
<td class="py-3 px-4 text-sm whitespace-nowrap">
|
|
<div class="flex items-center gap-1">
|
|
<x-ri-arrow-down-line class="size-3 text-blue-400" />
|
|
<span class="font-medium">{{ Illuminate\Support\Number::fileSize(bytes: $networkStats['netin']) }}</span>
|
|
</div>
|
|
@if($networkStats['netin_rate'] > 0)
|
|
<div class="text-xs text-base/50 mt-0.5">{{ Illuminate\Support\Number::fileSize(bytes: (int)$networkStats['netin_rate']) }}/s</div>
|
|
@endif
|
|
</td>
|
|
<td class="py-3 px-4 text-sm whitespace-nowrap">
|
|
<div class="flex items-center gap-1">
|
|
<x-ri-arrow-up-line class="size-3 text-orange-400" />
|
|
<span class="font-medium">{{ Illuminate\Support\Number::fileSize(bytes: $networkStats['netout']) }}</span>
|
|
</div>
|
|
@if($networkStats['netout_rate'] > 0)
|
|
<div class="text-xs text-base/50 mt-0.5">{{ Illuminate\Support\Number::fileSize(bytes: (int)$networkStats['netout_rate']) }}/s</div>
|
|
@endif
|
|
</td>
|
|
<td class="py-3 px-4 text-sm whitespace-nowrap">
|
|
<div class="flex items-center gap-3 text-xs">
|
|
<div class="flex items-center gap-1 whitespace-nowrap">
|
|
<x-ri-file-download-line class="size-3 text-purple-400" />
|
|
<span>{{ Illuminate\Support\Number::fileSize(bytes: $diskStats['diskread'] ?? 0) }}</span>
|
|
</div>
|
|
<div class="flex items-center gap-1 whitespace-nowrap">
|
|
<x-ri-file-upload-line class="size-3 text-purple-400" />
|
|
<span>{{ Illuminate\Support\Number::fileSize(bytes: $diskStats['diskwrite'] ?? 0) }}</span>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="py-3 px-4 text-sm whitespace-nowrap">
|
|
@if($stat->uptime > 0)
|
|
<span class="text-xs">{{ $this->formatUptime($stat->uptime) }}</span>
|
|
@else
|
|
<span class="text-base/50">-</span>
|
|
@endif
|
|
</td>
|
|
<td class="py-3 px-4 text-sm whitespace-nowrap">
|
|
<button
|
|
x-data
|
|
@click="$dispatch('open-delete-stat-modal', { id: {{ $stat->id }}, date: '{{ $stat->DateTime?->format('Y-m-d H:i:s') ?? '' }}', cpu: '{{ number_format($stat->cpumax > 0 ? ($stat->cpuused / $stat->cpumax * 100) : 0, 2) }}', memory: '{{ number_format($stat->memorymax > 0 ? ($stat->memoryused / $stat->memorymax * 100) : 0, 2) }}', disk: '{{ number_format(($stat->diskmax ?? 0) > 0 ? (($stat->diskused ?? 0) / ($stat->diskmax ?? 1) * 100) : 0, 2) }}', status: '{{ $stat->vmstatus ?? '' }}' })"
|
|
class="p-1.5 text-danger hover:text-danger/80 hover:bg-danger/10 rounded transition-colors"
|
|
title="Delete statistics entry"
|
|
>
|
|
<x-ri-delete-bin-line class="size-4" />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="10" class="py-12 text-center">
|
|
<div class="flex flex-col items-center gap-3">
|
|
<div class="p-3 bg-base/10 rounded-full">
|
|
<x-ri-database-line class="size-6 text-base/60" />
|
|
</div>
|
|
<div>
|
|
<p class="text-base font-semibold mb-1">No samples yet</p>
|
|
<p class="text-sm text-base/60">Hourly automation will populate this grid; you can also pin a manual capture.</p>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<template id="statistics-history-export" type="application/json">
|
|
@json($statisticsExportRows)
|
|
</template>
|
|
<!-- Statistics History Pagination -->
|
|
@if($this->statistics->total() > 0)
|
|
<div class="p-4 border-t border-neutral flex flex-col sm:flex-row items-center justify-between gap-4">
|
|
{{-- Records per page selector --}}
|
|
<div class="flex items-center gap-3">
|
|
<label class="text-sm text-base/70">Show:</label>
|
|
<select wire:model.live="statisticsPerPage" class="px-3 py-1.5 border border-neutral rounded-lg bg-background text-sm">
|
|
<option value="10">10</option>
|
|
<option value="25">25</option>
|
|
<option value="50">50</option>
|
|
<option value="100">100</option>
|
|
</select>
|
|
<span class="text-sm text-base/60">each page</span>
|
|
</div>
|
|
|
|
{{-- Page info and navigation --}}
|
|
<div class="flex items-center gap-4">
|
|
<span class="text-sm text-base/60">
|
|
Showing {{ (($this->statistics->currentPage() - 1) * $this->statistics->perPage()) + 1 }} - {{ min($this->statistics->currentPage() * $this->statistics->perPage(), $this->statistics->total()) }} of {{ $this->statistics->total() }} entries
|
|
</span>
|
|
|
|
@if($this->statistics->hasPages())
|
|
<div class="flex items-center gap-1">
|
|
{{-- First page --}}
|
|
<button type="button"
|
|
wire:click="gotoPage(1)"
|
|
@if($this->statistics->onFirstPage()) disabled @endif
|
|
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
title="First page">
|
|
<x-ri-arrow-left-double-line class="size-4" />
|
|
</button>
|
|
|
|
{{-- Previous page --}}
|
|
<button type="button"
|
|
wire:click="previousPage"
|
|
@if($this->statistics->onFirstPage()) disabled @endif
|
|
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
title="Previous page">
|
|
<x-ri-arrow-left-s-line class="size-4" />
|
|
</button>
|
|
|
|
{{-- Page numbers --}}
|
|
@php
|
|
$statsTotalPages = $this->statistics->lastPage();
|
|
$statsCurrentPage = $this->statistics->currentPage();
|
|
$statsStartPage = max(1, $statsCurrentPage - 2);
|
|
$statsEndPage = min($statsTotalPages, $statsCurrentPage + 2);
|
|
@endphp
|
|
|
|
@if($statsStartPage > 1)
|
|
<button type="button" wire:click="gotoPage(1)" class="px-3 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors text-sm">1</button>
|
|
@if($statsStartPage > 2)
|
|
<span class="px-2 text-base/60">...</span>
|
|
@endif
|
|
@endif
|
|
|
|
@for($i = $statsStartPage; $i <= $statsEndPage; $i++)
|
|
<button type="button"
|
|
wire:click="gotoPage({{ $i }})"
|
|
class="px-3 py-1 border rounded text-sm transition-colors {{ $statsCurrentPage === $i ? 'bg-primary text-white border-primary' : 'border-neutral hover:bg-background-secondary' }}">
|
|
{{ $i }}
|
|
</button>
|
|
@endfor
|
|
|
|
@if($statsEndPage < $statsTotalPages)
|
|
@if($statsEndPage < $statsTotalPages - 1)
|
|
<span class="px-2 text-base/60">...</span>
|
|
@endif
|
|
<button type="button" wire:click="gotoPage({{ $statsTotalPages }})" class="px-3 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors text-sm">{{ $statsTotalPages }}</button>
|
|
@endif
|
|
|
|
{{-- Next page --}}
|
|
<button type="button"
|
|
wire:click="nextPage"
|
|
@if($this->statistics->onLastPage()) disabled @endif
|
|
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
title="Next page">
|
|
<x-ri-arrow-right-s-line class="size-4" />
|
|
</button>
|
|
|
|
{{-- Last page --}}
|
|
<button type="button"
|
|
wire:click="gotoPage({{ $this->statistics->lastPage() }})"
|
|
@if($this->statistics->onLastPage()) disabled @endif
|
|
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
title="Last page">
|
|
<x-ri-arrow-right-double-line class="size-4" />
|
|
</button>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<script>
|
|
window.statisticsHistoryExportData = @json($statisticsExportRows ?? []);
|
|
</script>
|
|
|