mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(ui): move chart legends to the card header with last values
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0ad88d8c63
commit
6f8d51008e
@@ -402,6 +402,30 @@
|
||||
editorTheme = null
|
||||
}
|
||||
}
|
||||
// Chart legend in a card header: one entry per series with its last known value.
|
||||
// items: [{ name, color, data: [[timestamp, value], ...], formatter }]
|
||||
function renderChartLegend(element, items) {
|
||||
if (!element) return;
|
||||
element.replaceChildren(...items.map(({ name, color, data, formatter }) => {
|
||||
const last = (data || []).findLast(point => point?.[1] !== null && point?.[1] !== undefined);
|
||||
const item = document.createElement('span');
|
||||
item.className = 'inline-flex items-center gap-1.5';
|
||||
const dot = document.createElement('span');
|
||||
dot.className = 'size-2 shrink-0 rounded-full';
|
||||
dot.style.backgroundColor = color;
|
||||
const label = document.createElement('span');
|
||||
label.className = 'text-neutral-500 dark:text-fg-dim';
|
||||
label.textContent = name;
|
||||
item.append(dot, label);
|
||||
if (last) {
|
||||
const value = document.createElement('span');
|
||||
value.className = 'font-medium tabular-nums text-black dark:text-fg';
|
||||
value.textContent = formatter(last[1]);
|
||||
item.append(value);
|
||||
}
|
||||
return item;
|
||||
}));
|
||||
}
|
||||
@auth
|
||||
window.Pusher = Pusher;
|
||||
const EchoConstructor = typeof Echo === 'function' ? Echo : Echo.default;
|
||||
|
||||
@@ -231,6 +231,9 @@ $serverListboxOptions = array_merge(
|
||||
<div class="grid gap-6 lg:grid-cols-2">
|
||||
@foreach (['cpu' => 'CPU', 'memory' => 'Memory used', 'network' => 'Network throughput', 'load' => 'Load average', 'disk' => 'Disk usage'] as $key => $label)
|
||||
<x-application.settings-section :title="$label" @class(['lg:col-span-2' => $key === 'cpu'])>
|
||||
<x-slot:actions>
|
||||
<div wire:ignore id="fleet-chart-{{ $key }}-legend" class="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs"></div>
|
||||
</x-slot:actions>
|
||||
<div wire:ignore>
|
||||
<div id="fleet-chart-{{ $key }}" class="min-h-[240px] w-full"></div>
|
||||
</div>
|
||||
@@ -421,7 +424,7 @@ $serverListboxOptions = array_merge(
|
||||
fill: { type: 'gradient', gradient: { opacityFrom: 0.28, opacityTo: 0.02, stops: [0, 90, 100] } },
|
||||
dataLabels: { enabled: false },
|
||||
grid: { borderColor: 'rgba(128, 128, 128, 0.14)', strokeDashArray: 4 },
|
||||
legend: { show: series.length > 1, labels: { colors: axisColor } },
|
||||
legend: { show: false },
|
||||
xaxis: { type: 'datetime', labels: { datetimeUTC: true, style: { colors: axisColor } } },
|
||||
yaxis: { min: 0, max: max => max > 0 ? max * 1.2 : 1, forceNiceScale: true, tickAmount: 4, labels: { style: { colors: axisColor }, formatter } },
|
||||
noData: { text: 'No data for this range', style: { color: axisColor } },
|
||||
@@ -448,6 +451,15 @@ $serverListboxOptions = array_merge(
|
||||
charts[key] = chart;
|
||||
}
|
||||
|
||||
const renderLegends = specs => {
|
||||
for (const [key, spec] of Object.entries(specs)) {
|
||||
renderChartLegend(document.getElementById(`fleet-chart-${key}-legend`), spec.series.map((series, i) => ({
|
||||
name: series.name, color: spec.colors[i], data: series.data, formatter: spec.formatter,
|
||||
})));
|
||||
}
|
||||
};
|
||||
renderLegends(specs);
|
||||
|
||||
Livewire.on('refreshChartData-{!! $chartId !!}', payload => {
|
||||
if (typeof checkTheme === 'function') { checkTheme(); }
|
||||
const data = Array.isArray(payload) ? payload[0] : payload;
|
||||
@@ -456,6 +468,7 @@ $serverListboxOptions = array_merge(
|
||||
for (const [key, chart] of Object.entries(charts)) {
|
||||
chart.updateSeries(next[key].series);
|
||||
}
|
||||
renderLegends(next);
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -63,6 +63,9 @@
|
||||
|
||||
<x-application.settings-section id="cpu-metrics-section" title="CPU usage"
|
||||
helper="Percentage of available CPU capacity used by the application container.">
|
||||
<x-slot:actions>
|
||||
<div wire:ignore id="{!! $chartId !!}-cpu-legend" class="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs"></div>
|
||||
</x-slot:actions>
|
||||
<div wire:ignore id="{!! $chartId !!}-cpu" class="min-h-[240px] w-full"></div>
|
||||
|
||||
@script
|
||||
@@ -206,6 +209,12 @@
|
||||
},
|
||||
},
|
||||
});
|
||||
renderChartLegend(document.getElementById('{!! $chartId !!}-cpu-legend'), [{
|
||||
name: 'CPU',
|
||||
color: cpuColor,
|
||||
data: chartData[0].seriesData,
|
||||
formatter: value => `${Number(value.toFixed(Math.abs(value) < 1 ? 2 : 1))}%`,
|
||||
}]);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -214,6 +223,9 @@
|
||||
|
||||
<x-application.settings-section id="memory-metrics-section" title="Memory usage"
|
||||
helper="Memory consumed by the application container, measured in megabytes.">
|
||||
<x-slot:actions>
|
||||
<div wire:ignore id="{!! $chartId !!}-memory-legend" class="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs"></div>
|
||||
</x-slot:actions>
|
||||
<div wire:ignore id="{!! $chartId !!}-memory" class="min-h-[240px] w-full"></div>
|
||||
|
||||
@script
|
||||
@@ -349,6 +361,12 @@
|
||||
},
|
||||
},
|
||||
});
|
||||
renderChartLegend(document.getElementById('{!! $chartId !!}-memory-legend'), [{
|
||||
name: 'Memory',
|
||||
color: ramColor,
|
||||
data: chartData[0].seriesData,
|
||||
formatter: value => `${Math.round(value)} MB`,
|
||||
}]);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -64,26 +64,41 @@
|
||||
<div class="grid gap-6 lg:grid-cols-2">
|
||||
<x-application.settings-section id="server-cpu-metrics-section" title="CPU usage" class="lg:col-span-2"
|
||||
helper="Percentage of available CPU capacity used by this server.">
|
||||
<x-slot:actions>
|
||||
<div wire:ignore id="{!! $chartId !!}-cpu-legend" class="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs"></div>
|
||||
</x-slot:actions>
|
||||
<div wire:ignore id="{!! $chartId !!}-cpu" class="min-h-[240px] w-full"></div>
|
||||
</x-application.settings-section>
|
||||
|
||||
<x-application.settings-section id="server-memory-metrics-section" title="Memory usage"
|
||||
helper="Percentage of physical memory currently used by this server.">
|
||||
<x-slot:actions>
|
||||
<div wire:ignore id="{!! $chartId !!}-memory-legend" class="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs"></div>
|
||||
</x-slot:actions>
|
||||
<div wire:ignore id="{!! $chartId !!}-memory" class="min-h-[240px] w-full"></div>
|
||||
</x-application.settings-section>
|
||||
|
||||
<x-application.settings-section id="server-network-metrics-section" title="Network throughput"
|
||||
helper="Receive and transmit rate across the server's network interfaces.">
|
||||
<x-slot:actions>
|
||||
<div wire:ignore id="{!! $chartId !!}-network-legend" class="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs"></div>
|
||||
</x-slot:actions>
|
||||
<div wire:ignore id="{!! $chartId !!}-network" class="min-h-[240px] w-full"></div>
|
||||
</x-application.settings-section>
|
||||
|
||||
<x-application.settings-section id="server-load-metrics-section" title="Load average"
|
||||
helper="One minute load average reported by the server.">
|
||||
<x-slot:actions>
|
||||
<div wire:ignore id="{!! $chartId !!}-load-legend" class="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs"></div>
|
||||
</x-slot:actions>
|
||||
<div wire:ignore id="{!! $chartId !!}-load" class="min-h-[240px] w-full"></div>
|
||||
</x-application.settings-section>
|
||||
|
||||
<x-application.settings-section id="server-disk-metrics-section" title="Disk usage"
|
||||
helper="Percentage of the root filesystem used by this server.">
|
||||
<x-slot:actions>
|
||||
<div wire:ignore id="{!! $chartId !!}-disk-legend" class="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs"></div>
|
||||
</x-slot:actions>
|
||||
<div wire:ignore id="{!! $chartId !!}-disk" class="min-h-[240px] w-full"></div>
|
||||
</x-application.settings-section>
|
||||
</div>
|
||||
@@ -235,7 +250,6 @@
|
||||
{
|
||||
...chartOptions('Network', '#10b981', 'Loading network metrics…', formatRate),
|
||||
colors: ['#10b981', '#f59e0b'],
|
||||
legend: { show: true, labels: { colors: textColor } },
|
||||
},
|
||||
);
|
||||
const loadChart = new ApexCharts(
|
||||
@@ -243,6 +257,9 @@
|
||||
chartOptions('Load', '#14b8a6', 'Loading load metrics…', formatNumber),
|
||||
);
|
||||
|
||||
const renderLegend = (key, items) => renderChartLegend(
|
||||
document.getElementById(`{!! $chartId !!}-${key}-legend`), items);
|
||||
|
||||
// Defer to the next frame so the 2x2 grid cells are laid out
|
||||
// before ApexCharts measures them (a zero-width cell renders blank).
|
||||
requestAnimationFrame(() => {
|
||||
@@ -331,6 +348,15 @@
|
||||
{ name: 'TX', data: data.networkTxSeries || [] },
|
||||
]);
|
||||
loadChart.updateSeries([{ name: 'Load', data: data.loadSeries || [] }]);
|
||||
|
||||
renderLegend('cpu', [{ name: 'CPU', color: cpuColor, data: data.cpuSeries, formatter: formatPercent }]);
|
||||
renderLegend('memory', [{ name: 'Memory', color: ramColor, data: data.memorySeries, formatter: formatPercent }]);
|
||||
renderLegend('network', [
|
||||
{ name: 'RX', color: '#10b981', data: data.networkRxSeries, formatter: formatRate },
|
||||
{ name: 'TX', color: '#f59e0b', data: data.networkTxSeries, formatter: formatRate },
|
||||
]);
|
||||
renderLegend('load', [{ name: 'Load', color: '#14b8a6', data: data.loadSeries, formatter: formatNumber }]);
|
||||
renderLegend('disk', [{ name: 'Disk', color: '#ef4444', data: data.diskSeries, formatter: formatPercent }]);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user