Files
jabali-panel/resources/views/filament/admin/widgets/server-charts.blade.php
2026-01-24 19:36:46 +02:00

491 lines
25 KiB
PHP

<x-filament-widgets::widget wire:poll.10s="refreshData">
@php
$data = $this->getData();
$cpu = $data['cpu'] ?? [];
$memory = $data['memory'] ?? [];
$disk = $data['disk'] ?? [];
$partitions = $disk['partitions'] ?? [];
$load = $data['load'] ?? [];
$uptime = $data['uptime'] ?? 'N/A';
$cpuUsage = $cpu['usage'] ?? 0;
$memUsage = $memory['usage'] ?? 0;
// Memory values are in MB from agent
$memUsedGB = ($memory['used'] ?? 0) / 1024;
$memTotalGB = ($memory['total'] ?? 0) / 1024;
$partition = $partitions[0] ?? null;
$diskUsage = $partition ? round($partition['usage_percent'] ?? 0, 1) : 0;
$mountPoint = $partition['mount'] ?? '/';
$usedHuman = $partition['used_human'] ?? '0 B';
$totalHuman = $partition['total_human'] ?? '0 B';
@endphp
<style>
.server-charts-grid {
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
gap: 0;
}
@media (min-width: 640px) {
.server-charts-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
.server-charts-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.chart-card {
padding: 0.5rem;
}
.chart-container {
height: 280px;
width: 100%;
}
@media (min-width: 640px) {
.chart-container {
height: 320px;
}
}
@media (min-width: 1024px) {
.chart-container {
height: 360px;
}
}
.chart-label {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-top: -4rem;
}
.chart-title {
font-weight: 600;
font-size: 0.875rem;
color: rgb(17 24 39);
}
.dark .chart-title {
color: white;
}
.chart-subtitle {
font-size: 0.75rem;
color: rgb(107 114 128);
text-align: center;
margin-top: 0.25rem;
}
.dark .chart-subtitle {
color: rgb(156 163 175);
}
</style>
{{-- Main Charts Grid (3 columns) --}}
<div class="server-charts-grid">
{{-- CPU Usage Chart --}}
<div class="chart-card">
<div
x-data="{
chart: null,
value: {{ $cpuUsage }},
init() {
this.initChart();
Livewire.on('server-charts-updated', (data) => {
const newValue = data[0]?.cpu ?? data.cpu;
if (this.chart && newValue !== undefined) {
this.value = newValue;
const color = this.value >= 90 ? '#ef4444' : (this.value >= 70 ? '#f59e0b' : '#22c55e');
const shadowColor = this.value >= 90 ? 'rgba(239,68,68,0.45)' : (this.value >= 70 ? 'rgba(245,158,11,0.45)' : 'rgba(34,197,94,0.45)');
this.chart.setOption({
series: [{
itemStyle: { color: color, shadowColor: shadowColor },
data: [{ value: this.value }]
}]
});
}
});
},
initChart() {
if (typeof window.echarts === 'undefined') {
setTimeout(() => this.initChart(), 100);
return;
}
const isDark = document.documentElement.classList.contains('dark');
this.chart = echarts.init(this.$refs.chart);
const color = this.value >= 90 ? '#ef4444' : (this.value >= 70 ? '#f59e0b' : '#22c55e');
const shadowColor = this.value >= 90 ? 'rgba(239,68,68,0.45)' : (this.value >= 70 ? 'rgba(245,158,11,0.45)' : 'rgba(34,197,94,0.45)');
this.chart.setOption({
series: [{
type: 'gauge',
startAngle: 180,
endAngle: 0,
min: 0,
max: 100,
splitNumber: 10,
itemStyle: {
color: color,
shadowColor: shadowColor,
shadowBlur: 10,
shadowOffsetX: 2,
shadowOffsetY: 2
},
progress: {
show: true,
roundCap: true,
width: 10
},
pointer: {
icon: 'path://M2090.36389,615.30999 L2090.36389,615.30999 C2091.48372,615.30999 2092.40383,616.194028 2092.44859,617.312956 L2096.90698,728.755929 C2097.05155,732.369577 2094.2393,735.416212 2090.62566,735.56078 C2090.53845,735.564269 2090.45117,735.566014 2090.36389,735.566014 L2090.36389,735.566014 C2086.74736,735.566014 2083.81557,732.63423 2083.81557,729.017692 C2083.81557,728.930412 2083.81732,728.84314 2083.82081,728.755929 L2088.2792,617.312956 C2088.32396,616.194028 2089.24407,615.30999 2090.36389,615.30999 Z',
length: '75%',
width: 16,
offsetCenter: [0, '5%']
},
axisLine: {
roundCap: true,
lineStyle: {
width: 10,
color: [[1, isDark ? '#374151' : '#e5e7eb']]
}
},
axisTick: {
splitNumber: 2,
lineStyle: {
width: 2,
color: isDark ? '#6b7280' : '#999'
}
},
splitLine: {
length: 12,
lineStyle: {
width: 3,
color: isDark ? '#6b7280' : '#999'
}
},
axisLabel: {
distance: 30,
color: isDark ? '#9ca3af' : '#999',
fontSize: 14
},
title: {
show: false
},
detail: {
backgroundColor: isDark ? '#1f2937' : '#fff',
borderColor: isDark ? '#4b5563' : '#999',
borderWidth: 2,
width: '40%',
lineHeight: 24,
height: 24,
borderRadius: 8,
offsetCenter: [0, '35%'],
valueAnimation: true,
formatter: function (value) {
return '{value|' + value.toFixed(0) + '}{unit|%}';
},
rich: {
value: {
fontSize: 20,
fontWeight: 'bolder',
color: isDark ? '#e5e7eb' : '#777'
},
unit: {
fontSize: 10,
color: isDark ? '#9ca3af' : '#999',
padding: [0, 0, -3, 2]
}
}
},
data: [{ value: this.value }]
}]
});
window.addEventListener('resize', () => this.chart?.resize());
},
destroy() {
this.chart?.dispose();
}
}"
wire:ignore
>
<div x-ref="chart" class="chart-container"></div>
</div>
<div class="chart-label">
<x-filament::icon icon="heroicon-o-cpu-chip" class="h-5 w-5 text-primary-500" />
<span class="chart-title">{{ __('CPU Usage') }}</span>
</div>
<div class="chart-subtitle">{{ $cpu['cores'] ?? 0 }} {{ __('cores') }} · {{ __('Load') }}: {{ $load['1min'] ?? 0 }} / {{ $load['5min'] ?? 0 }} / {{ $load['15min'] ?? 0 }}</div>
</div>
{{-- Memory Usage Chart --}}
<div class="chart-card">
<div
x-data="{
chart: null,
value: {{ $memUsage }},
init() {
this.initChart();
Livewire.on('server-charts-updated', (data) => {
const newValue = data[0]?.memory ?? data.memory;
if (this.chart && newValue !== undefined) {
this.value = newValue;
const color = this.value >= 90 ? '#ef4444' : (this.value >= 70 ? '#f59e0b' : '#22c55e');
const shadowColor = this.value >= 90 ? 'rgba(239,68,68,0.45)' : (this.value >= 70 ? 'rgba(245,158,11,0.45)' : 'rgba(34,197,94,0.45)');
this.chart.setOption({
series: [{
itemStyle: { color: color, shadowColor: shadowColor },
data: [{ value: this.value }]
}]
});
}
});
},
initChart() {
if (typeof window.echarts === 'undefined') {
setTimeout(() => this.initChart(), 100);
return;
}
const isDark = document.documentElement.classList.contains('dark');
this.chart = echarts.init(this.$refs.chart);
const color = this.value >= 90 ? '#ef4444' : (this.value >= 70 ? '#f59e0b' : '#22c55e');
const shadowColor = this.value >= 90 ? 'rgba(239,68,68,0.45)' : (this.value >= 70 ? 'rgba(245,158,11,0.45)' : 'rgba(34,197,94,0.45)');
this.chart.setOption({
series: [{
type: 'gauge',
startAngle: 180,
endAngle: 0,
min: 0,
max: 100,
splitNumber: 10,
itemStyle: {
color: color,
shadowColor: shadowColor,
shadowBlur: 10,
shadowOffsetX: 2,
shadowOffsetY: 2
},
progress: {
show: true,
roundCap: true,
width: 10
},
pointer: {
icon: 'path://M2090.36389,615.30999 L2090.36389,615.30999 C2091.48372,615.30999 2092.40383,616.194028 2092.44859,617.312956 L2096.90698,728.755929 C2097.05155,732.369577 2094.2393,735.416212 2090.62566,735.56078 C2090.53845,735.564269 2090.45117,735.566014 2090.36389,735.566014 L2090.36389,735.566014 C2086.74736,735.566014 2083.81557,732.63423 2083.81557,729.017692 C2083.81557,728.930412 2083.81732,728.84314 2083.82081,728.755929 L2088.2792,617.312956 C2088.32396,616.194028 2089.24407,615.30999 2090.36389,615.30999 Z',
length: '75%',
width: 16,
offsetCenter: [0, '5%']
},
axisLine: {
roundCap: true,
lineStyle: {
width: 10,
color: [[1, isDark ? '#374151' : '#e5e7eb']]
}
},
axisTick: {
splitNumber: 2,
lineStyle: {
width: 2,
color: isDark ? '#6b7280' : '#999'
}
},
splitLine: {
length: 12,
lineStyle: {
width: 3,
color: isDark ? '#6b7280' : '#999'
}
},
axisLabel: {
distance: 30,
color: isDark ? '#9ca3af' : '#999',
fontSize: 14
},
title: {
show: false
},
detail: {
backgroundColor: isDark ? '#1f2937' : '#fff',
borderColor: isDark ? '#4b5563' : '#999',
borderWidth: 2,
width: '40%',
lineHeight: 24,
height: 24,
borderRadius: 8,
offsetCenter: [0, '35%'],
valueAnimation: true,
formatter: function (value) {
return '{value|' + value.toFixed(0) + '}{unit|%}';
},
rich: {
value: {
fontSize: 20,
fontWeight: 'bolder',
color: isDark ? '#e5e7eb' : '#777'
},
unit: {
fontSize: 10,
color: isDark ? '#9ca3af' : '#999',
padding: [0, 0, -3, 2]
}
}
},
data: [{ value: this.value }]
}]
});
window.addEventListener('resize', () => this.chart?.resize());
},
destroy() {
this.chart?.dispose();
}
}"
wire:ignore
>
<div x-ref="chart" class="chart-container"></div>
</div>
<div class="chart-label">
<x-filament::icon icon="heroicon-o-server" class="h-5 w-5 text-info-500" />
<span class="chart-title">{{ __('Memory') }}</span>
</div>
<div class="chart-subtitle">{{ number_format($memUsedGB, 1) }} GB {{ __('used') }} / {{ number_format($memTotalGB, 1) }} GB {{ __('total') }}</div>
</div>
{{-- Disk Usage Chart --}}
@if($partition)
<div class="chart-card">
<div
x-data="{
chart: null,
value: {{ $diskUsage }},
init() {
this.initChart();
Livewire.on('server-charts-updated', (data) => {
const newValue = data[0]?.disk ?? data.disk;
if (this.chart && newValue !== undefined) {
this.value = newValue;
const color = this.value >= 90 ? '#ef4444' : (this.value >= 70 ? '#f59e0b' : '#22c55e');
const shadowColor = this.value >= 90 ? 'rgba(239,68,68,0.45)' : (this.value >= 70 ? 'rgba(245,158,11,0.45)' : 'rgba(34,197,94,0.45)');
this.chart.setOption({
series: [{
itemStyle: { color: color, shadowColor: shadowColor },
data: [{ value: this.value }]
}]
});
}
});
},
initChart() {
if (typeof window.echarts === 'undefined') {
setTimeout(() => this.initChart(), 100);
return;
}
const isDark = document.documentElement.classList.contains('dark');
this.chart = echarts.init(this.$refs.chart);
const color = this.value >= 90 ? '#ef4444' : (this.value >= 70 ? '#f59e0b' : '#22c55e');
const shadowColor = this.value >= 90 ? 'rgba(239,68,68,0.45)' : (this.value >= 70 ? 'rgba(245,158,11,0.45)' : 'rgba(34,197,94,0.45)');
this.chart.setOption({
series: [{
type: 'gauge',
startAngle: 180,
endAngle: 0,
min: 0,
max: 100,
splitNumber: 10,
itemStyle: {
color: color,
shadowColor: shadowColor,
shadowBlur: 10,
shadowOffsetX: 2,
shadowOffsetY: 2
},
progress: {
show: true,
roundCap: true,
width: 10
},
pointer: {
icon: 'path://M2090.36389,615.30999 L2090.36389,615.30999 C2091.48372,615.30999 2092.40383,616.194028 2092.44859,617.312956 L2096.90698,728.755929 C2097.05155,732.369577 2094.2393,735.416212 2090.62566,735.56078 C2090.53845,735.564269 2090.45117,735.566014 2090.36389,735.566014 L2090.36389,735.566014 C2086.74736,735.566014 2083.81557,732.63423 2083.81557,729.017692 C2083.81557,728.930412 2083.81732,728.84314 2083.82081,728.755929 L2088.2792,617.312956 C2088.32396,616.194028 2089.24407,615.30999 2090.36389,615.30999 Z',
length: '75%',
width: 16,
offsetCenter: [0, '5%']
},
axisLine: {
roundCap: true,
lineStyle: {
width: 10,
color: [[1, isDark ? '#374151' : '#e5e7eb']]
}
},
axisTick: {
splitNumber: 2,
lineStyle: {
width: 2,
color: isDark ? '#6b7280' : '#999'
}
},
splitLine: {
length: 12,
lineStyle: {
width: 3,
color: isDark ? '#6b7280' : '#999'
}
},
axisLabel: {
distance: 30,
color: isDark ? '#9ca3af' : '#999',
fontSize: 14
},
title: {
show: false
},
detail: {
backgroundColor: isDark ? '#1f2937' : '#fff',
borderColor: isDark ? '#4b5563' : '#999',
borderWidth: 2,
width: '40%',
lineHeight: 24,
height: 24,
borderRadius: 8,
offsetCenter: [0, '35%'],
valueAnimation: true,
formatter: function (value) {
return '{value|' + value.toFixed(0) + '}{unit|%}';
},
rich: {
value: {
fontSize: 20,
fontWeight: 'bolder',
color: isDark ? '#e5e7eb' : '#777'
},
unit: {
fontSize: 10,
color: isDark ? '#9ca3af' : '#999',
padding: [0, 0, -3, 2]
}
}
},
data: [{ value: this.value }]
}]
});
window.addEventListener('resize', () => this.chart?.resize());
},
destroy() {
this.chart?.dispose();
}
}"
wire:ignore
>
<div x-ref="chart" class="chart-container"></div>
</div>
<div class="chart-label">
<x-filament::icon icon="heroicon-o-circle-stack" class="h-5 w-5 text-warning-500" />
<span class="chart-title">{{ __('Disk') }} {{ $mountPoint }}</span>
</div>
<div class="chart-subtitle">{{ $usedHuman }} {{ __('used') }} / {{ $totalHuman }} {{ __('total') }}</div>
</div>
@endif
</div>
</x-filament-widgets::widget>