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 }]);
});
})();