From 6f8d51008e55875501dff76425319db1b2d8e6bd Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 24 Oct 2026 12:49:23 +0200 Subject: [PATCH] fix(ui): move chart legends to the card header with last values Co-Authored-By: Claude Opus 5.5 --- resources/views/layouts/base.blade.php | 24 ++++++++++++++++ .../views/livewire/fleet/metrics.blade.php | 15 +++++++++- .../livewire/project/shared/metrics.blade.php | 18 ++++++++++++ .../views/livewire/server/charts.blade.php | 28 ++++++++++++++++++- 4 files changed, 83 insertions(+), 2 deletions(-) diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index d3bac09361..560dc4bee7 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -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; diff --git a/resources/views/livewire/fleet/metrics.blade.php b/resources/views/livewire/fleet/metrics.blade.php index b215ed1f17..71c40ca9a1 100644 --- a/resources/views/livewire/fleet/metrics.blade.php +++ b/resources/views/livewire/fleet/metrics.blade.php @@ -231,6 +231,9 @@ $serverListboxOptions = array_merge(
@foreach (['cpu' => 'CPU', 'memory' => 'Memory used', 'network' => 'Network throughput', 'load' => 'Load average', 'disk' => 'Disk usage'] as $key => $label) $key === 'cpu'])> + +
+
@@ -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); }); }); })(); diff --git a/resources/views/livewire/project/shared/metrics.blade.php b/resources/views/livewire/project/shared/metrics.blade.php index 17f89523e2..aef98426a9 100644 --- a/resources/views/livewire/project/shared/metrics.blade.php +++ b/resources/views/livewire/project/shared/metrics.blade.php @@ -63,6 +63,9 @@ + +
+
@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))}%`, + }]); }); })(); @@ -214,6 +223,9 @@ + +
+
@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`, + }]); }); })(); diff --git a/resources/views/livewire/server/charts.blade.php b/resources/views/livewire/server/charts.blade.php index 1d889a2ac6..c1b1992757 100644 --- a/resources/views/livewire/server/charts.blade.php +++ b/resources/views/livewire/server/charts.blade.php @@ -64,26 +64,41 @@
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
@@ -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 }]); }); })();