384 lines
22 KiB
PHP
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>
|