Files
jabali-panel/resources/views/filament/admin/pages/resource-usage.blade.php
2026-01-27 23:38:27 +02:00

384 lines
22 KiB
PHP

<x-filament-panels::page>
@once
@vite('resources/js/server-charts.js')
@endonce
{{ $this->usageForm }}
@php($hasRealData = !empty($chartData) && !empty($chartData['labels']))
@if(! $hasRealData)
<x-filament::section class="mt-6">
<div class="flex flex-col items-center justify-center py-10">
<div class="mb-3 rounded-full bg-gray-100 p-3 dark:bg-gray-500/20">
<x-filament::icon icon="heroicon-o-chart-bar" class="h-6 w-6 text-gray-500 dark:text-gray-400" />
</div>
<h3 class="text-base font-semibold text-gray-950 dark:text-white">
{{ __('No usage data yet') }}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ __('Usage snapshots are collected hourly. Data will appear after the first collection run.') }}
</p>
</div>
</x-filament::section>
@else
<div class="mt-6 space-y-6">
<x-filament::section icon="heroicon-o-square-3-stack-3d" icon-color="primary">
<x-slot name="heading">{{ __('Latest Snapshot') }}</x-slot>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-white/10 dark:bg-white/5">
<p class="text-sm text-gray-500 dark:text-gray-400">{{ __('Disk') }}</p>
<p class="text-lg font-semibold text-gray-950 dark:text-white">{{ $summary['disk'] ?? __('No data') }}</p>
</div>
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-white/10 dark:bg-white/5">
<p class="text-sm text-gray-500 dark:text-gray-400">{{ __('Mail') }}</p>
<p class="text-lg font-semibold text-gray-950 dark:text-white">{{ $summary['mail'] ?? __('No data') }}</p>
</div>
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-white/10 dark:bg-white/5">
<p class="text-sm text-gray-500 dark:text-gray-400">{{ __('Databases') }}</p>
<p class="text-lg font-semibold text-gray-950 dark:text-white">{{ $summary['database'] ?? __('No data') }}</p>
</div>
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-white/10 dark:bg-white/5">
<p class="text-sm text-gray-500 dark:text-gray-400">{{ __('Bandwidth (last hour)') }}</p>
<p class="text-lg font-semibold text-gray-950 dark:text-white">{{ $summary['bandwidth'] ?? __('No data') }}</p>
</div>
</div>
</x-filament::section>
<x-filament::section icon="heroicon-o-chart-bar" icon-color="success">
<x-slot name="heading">{{ __('Resource Usage (Last 30 Days)') }}</x-slot>
<x-slot name="description">{{ __('Historical snapshots collected hourly.') }}</x-slot>
<div
wire:key="resource-usage-chart-{{ $usageFormData['user_id'] ?? 'none' }}"
x-data="{
chart: null,
init() {
let data = @js($chartData);
const boot = () => {
const element = this.$refs.chart ?? this.$el;
if (!window.echarts || !data.labels?.length || !element) {
return false;
}
if (this.chart) {
this.chart.dispose();
}
this.chart = window.echarts.init(element);
this.chart.setOption({
tooltip: { trigger: 'axis' },
legend: {
data: ['Disk', 'Mail', 'Databases', 'Bandwidth'],
bottom: 0,
},
grid: { left: '3%', right: '4%', bottom: '12%', containLabel: true },
xAxis: {
type: 'category',
data: data.labels,
axisLabel: {
formatter: (value) => value.slice(5),
},
},
yAxis: {
type: 'value',
axisLabel: { formatter: '{value} GB' },
},
series: [
{ name: 'Disk', type: 'line', smooth: true, data: data.disk, areaStyle: {} },
{ name: 'Mail', type: 'line', smooth: true, data: data.mail, areaStyle: {} },
{ name: 'Databases', type: 'line', smooth: true, data: data.database, areaStyle: {} },
{ name: 'Bandwidth', type: 'line', smooth: true, data: data.bandwidth, areaStyle: {} },
],
});
window.addEventListener('resize', () => this.chart?.resize());
requestAnimationFrame(() => this.chart?.resize());
setTimeout(() => this.chart?.resize(), 150);
return true;
};
if (!boot()) {
const interval = setInterval(() => {
if (boot()) {
clearInterval(interval);
}
}, 200);
}
}
}"
x-init="init"
class="w-full"
wire:ignore
>
<div x-ref="chart" class="h-80 w-full" style="height: 320px;"></div>
</div>
</x-filament::section>
@if($hasPerformanceData)
<div class="grid gap-6 lg:grid-cols-2">
<x-filament::section icon="heroicon-o-cpu-chip" icon-color="info">
<x-slot name="heading">{{ __('CPU Usage (Last 30 Days)') }}</x-slot>
<x-slot name="description">{{ __('Average CPU percent per snapshot.') }}</x-slot>
<div class="mb-4 flex flex-wrap gap-4 text-sm text-gray-600 dark:text-gray-300">
<div>
<span class="font-semibold text-gray-900 dark:text-white">{{ __('Limit') }}:</span>
{{ $cpuLimitPercent ? $cpuLimitPercent . '%' : __('Unlimited') }}
</div>
<div>
<span class="font-semibold text-gray-900 dark:text-white">{{ __('Average') }}:</span>
{{ $cpuStats['avg'] ?? __('No data') }}
</div>
<div>
<span class="font-semibold text-gray-900 dark:text-white">{{ __('Peak') }}:</span>
{{ $cpuStats['max'] ?? __('No data') }}
</div>
</div>
<div
x-data="{
chart: null,
init() {
const data = @js($performanceChartData);
const limit = @js($cpuLimitPercent);
const boot = () => {
const element = this.$refs.chart ?? this.$el;
if (!window.echarts || !data.labels?.length || !element) {
return false;
}
if (this.chart) {
this.chart.dispose();
}
const series = [
{ name: 'CPU', type: 'line', smooth: true, data: data.cpu, areaStyle: {} },
];
if (limit) {
series.push({
name: 'Limit',
type: 'line',
data: data.labels.map(() => limit),
lineStyle: { type: 'dashed', width: 2, color: '#ef4444' },
symbol: 'none',
});
}
this.chart = window.echarts.init(element);
this.chart.setOption({
tooltip: { trigger: 'axis' },
legend: { data: series.map(item => item.name), bottom: 0 },
grid: { left: '3%', right: '4%', bottom: 50, containLabel: true },
xAxis: {
type: 'category',
data: data.labels,
axisLabel: {
formatter: (value) => value.slice(5),
margin: 12,
},
},
yAxis: {
type: 'value',
axisLabel: { formatter: '{value}%' },
},
series,
});
window.addEventListener('resize', () => this.chart?.resize());
requestAnimationFrame(() => this.chart?.resize());
setTimeout(() => this.chart?.resize(), 150);
return true;
};
if (!boot()) {
const interval = setInterval(() => {
if (boot()) {
clearInterval(interval);
}
}, 200);
}
}
}"
x-init="init"
class="w-full"
wire:ignore
>
<div x-ref="chart" class="h-80 w-full" style="height: 320px;"></div>
</div>
</x-filament::section>
<x-filament::section icon="heroicon-o-circle-stack" icon-color="warning">
<x-slot name="heading">{{ __('Memory Usage (Last 30 Days)') }}</x-slot>
<x-slot name="description">{{ __('Resident memory per snapshot.') }}</x-slot>
<div class="mb-4 flex flex-wrap gap-4 text-sm text-gray-600 dark:text-gray-300">
<div>
<span class="font-semibold text-gray-900 dark:text-white">{{ __('Limit') }}:</span>
{{ $memoryLimitGb ? number_format($memoryLimitGb, 2) . ' GB' : __('Unlimited') }}
</div>
<div>
<span class="font-semibold text-gray-900 dark:text-white">{{ __('Average') }}:</span>
{{ $memoryStats['avg'] ?? __('No data') }}
</div>
<div>
<span class="font-semibold text-gray-900 dark:text-white">{{ __('Peak') }}:</span>
{{ $memoryStats['max'] ?? __('No data') }}
</div>
</div>
<div
x-data="{
chart: null,
init() {
const data = @js($performanceChartData);
const limit = @js($memoryLimitGb);
const boot = () => {
const element = this.$refs.chart ?? this.$el;
if (!window.echarts || !data.labels?.length || !element) {
return false;
}
if (this.chart) {
this.chart.dispose();
}
const series = [
{ name: 'Memory', type: 'line', smooth: true, data: data.memory, areaStyle: {} },
];
if (limit) {
series.push({
name: 'Limit',
type: 'line',
data: data.labels.map(() => limit),
lineStyle: { type: 'dashed', width: 2, color: '#ef4444' },
symbol: 'none',
});
}
this.chart = window.echarts.init(element);
this.chart.setOption({
tooltip: { trigger: 'axis' },
legend: { data: series.map(item => item.name), bottom: 0 },
grid: { left: '3%', right: '4%', bottom: 50, containLabel: true },
xAxis: {
type: 'category',
data: data.labels,
axisLabel: {
formatter: (value) => value.slice(5),
margin: 12,
},
},
yAxis: {
type: 'value',
axisLabel: { formatter: '{value} GB' },
},
series,
});
window.addEventListener('resize', () => this.chart?.resize());
requestAnimationFrame(() => this.chart?.resize());
setTimeout(() => this.chart?.resize(), 150);
return true;
};
if (!boot()) {
const interval = setInterval(() => {
if (boot()) {
clearInterval(interval);
}
}, 200);
}
}
}"
x-init="init"
class="w-full"
wire:ignore
>
<div x-ref="chart" class="h-80 w-full" style="height: 320px;"></div>
</div>
</x-filament::section>
<x-filament::section icon="heroicon-o-arrow-path-rounded-square" icon-color="primary">
<x-slot name="heading">{{ __('Disk IO (Last 30 Days)') }}</x-slot>
<x-slot name="description">{{ __('Read/write MB per snapshot.') }}</x-slot>
<div
x-data="{
chart: null,
init() {
const data = @js($performanceChartData);
const boot = () => {
const element = this.$refs.chart ?? this.$el;
if (!window.echarts || !data.labels?.length || !element) {
return false;
}
if (this.chart) {
this.chart.dispose();
}
this.chart = window.echarts.init(element);
this.chart.setOption({
tooltip: { trigger: 'axis' },
legend: { data: ['Disk IO'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: 50, containLabel: true },
xAxis: {
type: 'category',
data: data.labels,
axisLabel: {
formatter: (value) => value.slice(5),
margin: 12,
},
},
yAxis: {
type: 'value',
axisLabel: { formatter: '{value} MB' },
},
series: [
{ name: 'Disk IO', type: 'line', smooth: true, data: data.disk_io, areaStyle: {} },
],
});
window.addEventListener('resize', () => this.chart?.resize());
requestAnimationFrame(() => this.chart?.resize());
setTimeout(() => this.chart?.resize(), 150);
return true;
};
if (!boot()) {
const interval = setInterval(() => {
if (boot()) {
clearInterval(interval);
}
}, 200);
}
}
}"
x-init="init"
class="w-full"
wire:ignore
>
<div x-ref="chart" class="h-80 w-full" style="height: 320px;"></div>
</div>
</x-filament::section>
</div>
@else
<x-filament::section icon="heroicon-o-cpu-chip" icon-color="info">
<x-slot name="heading">{{ __('CPU, Memory & Disk IO') }}</x-slot>
<div class="flex flex-col items-center justify-center py-10 text-center">
<div class="mb-3 rounded-full bg-gray-100 p-3 dark:bg-gray-500/20">
<x-filament::icon icon="heroicon-o-cpu-chip" class="h-6 w-6 text-gray-500 dark:text-gray-400" />
</div>
<h3 class="text-base font-semibold text-gray-950 dark:text-white">
{{ __('No performance data yet') }}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ __('CPU, memory, and disk IO snapshots will appear after the next collection run.') }}
</p>
</div>
</x-filament::section>
@endif
</div>
@endif
</x-filament-panels::page>