fix(metrics): rAF chart render, container image resolution, tab loading state, hi-fi skeleton

This commit is contained in:
Aditya Tripathi
2026-09-16 09:29:32 +00:00
parent 6b0c97211a
commit 90e6efadad
3 changed files with 82 additions and 39 deletions
+23 -14
View File
@@ -143,8 +143,9 @@ class Metrics extends Component
);
foreach ($current as $c) {
$meta = $this->resolveContainer($server, $c['id']);
$containers[] = array_merge($c, ['server' => $server->name, 'name' => $meta['name'], 'link' => $meta['link']]);
// Names/images are resolved lazily for the displayed rows only (containerMeta),
// so a server with many containers doesn't trigger a resolve storm here.
$containers[] = array_merge($c, ['server' => $server->name]);
}
$series['cpu'][] = $client->history('cpu', $from);
@@ -185,30 +186,38 @@ class Metrics extends Component
/**
* Resolve a Sentinel container id (the resource uuid Sentinel keys by) to a
* team-owned resource name + metrics link, memoized. Shows the raw id when no
* team-owned resource matches, so a Sentinel id never discloses another team's name.
* team-owned resource name, docker image, and metrics link, memoized. Shows the raw
* id and no image when no team-owned resource matches, so a Sentinel id never
* discloses another team's resource.
*
* @return array{name: string, link: ?string}
* @return array{name: string, image: ?string, link: ?string}
*/
protected function resolveContainer(Server $server, string $id): array
public function containerMeta(string $id): array
{
if (isset($this->containerMetaCache[$id])) {
return $this->containerMetaCache[$id];
}
$app = Application::ownedByCurrentTeam()->with('environment.project')->whereUuid($id)->first();
$teamId = currentTeam()?->id;
$resource = $teamId ? getResourceByUuid($id, $teamId) : null;
$image = null;
$link = null;
if ($app && data_get($app, 'environment.project.uuid')) {
$link = route('project.application.metrics', [
'project_uuid' => $app->environment->project->uuid,
'environment_uuid' => $app->environment->uuid,
'application_uuid' => $app->uuid,
]);
if ($resource) {
$image = data_get($resource, 'docker_registry_image_name') ?: data_get($resource, 'image');
if ($resource instanceof Application && data_get($resource, 'environment.project.uuid')) {
$link = route('project.application.metrics', [
'project_uuid' => $resource->environment->project->uuid,
'environment_uuid' => $resource->environment->uuid,
'application_uuid' => $resource->uuid,
]);
}
}
return $this->containerMetaCache[$id] = [
'name' => $app?->name ?? $id,
'name' => $resource?->name ?? $id,
'image' => $image ?: null,
'link' => $link,
];
}
@@ -1,18 +1,38 @@
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-1">
<h1 class="text-2xl font-semibold text-black dark:text-fg">Metrics</h1>
<p class="text-[13px] text-neutral-500 dark:text-fg-dim">Resource usage across your servers.</p>
<div class="flex w-full min-w-0 flex-col gap-6">
{{-- Header (real chrome; only the data below is a skeleton) --}}
<div class="flex flex-col gap-4">
<div class="min-w-0">
<h1 class="min-w-0 text-[24px]! leading-7! font-semibold! tracking-tight!">Metrics</h1>
<p class="mt-1 text-[13px] text-neutral-500 dark:text-fg-dim">
Resource usage across every server, reported by Sentinel.
</p>
</div>
{{-- Filter bar --}}
<div class="flex flex-wrap items-center gap-2">
<x-skeleton class="h-9 w-full rounded-lg sm:w-52" />
<x-skeleton class="h-9 w-52 rounded-lg sm:ml-auto" />
</div>
</div>
<x-skeleton.tiles :count="6" grid="grid-cols-2 sm:grid-cols-3 lg:grid-cols-6"
:labels="['Servers online', 'Fleet CPU', 'Fleet memory', 'Fleet disk', 'Fleet network', 'Containers']" />
<x-application.settings-section id="metrics-overview-section" title="Overview">
<x-skeleton.tiles :count="6" grid="grid-cols-2 sm:grid-cols-3 lg:grid-cols-6"
:labels="['Servers online', 'Fleet CPU', 'Fleet memory', 'Fleet disk', 'Fleet network', 'Containers']" />
</x-application.settings-section>
<div class="grid gap-6 lg:grid-cols-2">
@foreach (['CPU', 'Memory used'] as $label)
<div class="rounded-lg bg-[var(--coollabs-elevated)] p-4 ring-1 ring-[var(--coollabs-hairline)]">
<span class="text-[13px] font-medium text-black dark:text-fg">{{ $label }}</span>
<x-skeleton class="mt-3 h-[220px] w-full rounded" />
</div>
@foreach (['CPU', 'Memory used', 'Network throughput', 'Load average', 'Disk usage'] as $label)
<x-application.settings-section :title="$label">
<x-skeleton class="h-[240px] w-full rounded-lg" />
</x-application.settings-section>
@endforeach
</div>
<x-application.settings-section title="Servers" flush>
<x-skeleton.table :rows="4" flush />
</x-application.settings-section>
<x-application.settings-section title="Hottest containers" flush>
<x-skeleton.table :rows="6" flush />
</x-application.settings-section>
</div>
@@ -174,13 +174,18 @@ $serverListboxOptions = array_merge(
<x-application.settings-section title="Hottest containers" flush
helper="Top containers across the fleet by the selected metric.">
<x-slot:actions>
<div class="inline-flex items-center gap-0.5 rounded-lg bg-neutral-100 p-1 dark:bg-white/[0.04]">
@foreach (['cpu' => 'CPU', 'memory' => 'Memory', 'disk' => 'Disk', 'network' => 'Network'] as $m => $mlabel)
<button type="button" wire:click="$set('containerMetric', '{{ $m }}')"
@class([$tabButtonBase, $containerMetric === $m ? $tabButtonActive : $tabButtonInactive])>
{{ $mlabel }}
</button>
@endforeach
<div class="flex items-center gap-2">
<x-loading compact class="text-neutral-400 dark:text-fg-faint"
wire:loading.flex wire:target="containerMetric" />
<div class="inline-flex items-center gap-0.5 rounded-lg bg-neutral-100 p-1 dark:bg-white/[0.04]">
@foreach (['cpu' => 'CPU', 'memory' => 'Memory', 'disk' => 'Disk', 'network' => 'Network'] as $m => $mlabel)
<button type="button" wire:click="$set('containerMetric', '{{ $m }}')"
wire:loading.attr="disabled" wire:target="containerMetric"
@class([$tabButtonBase, $containerMetric === $m ? $tabButtonActive : $tabButtonInactive])>
{{ $mlabel }}
</button>
@endforeach
</div>
</div>
</x-slot:actions>
@if (empty($topContainers))
@@ -189,23 +194,28 @@ $serverListboxOptions = array_merge(
description="Container metrics need a Sentinel build with the bulk endpoints." />
</div>
@else
<div class="data-table">
<div class="data-table transition-opacity" wire:loading.class="pointer-events-none opacity-50"
wire:target="containerMetric">
<div class="data-table-header fleet-containers-table-grid">
<span>Container</span>
<span>Server</span>
<span class="text-right">{{ ['cpu' => 'CPU', 'memory' => 'Memory', 'disk' => 'Disk', 'network' => 'Network'][$containerMetric] }}</span>
</div>
@foreach ($topContainers as $c)
@php $meta = $this->containerMeta($c['id']); @endphp
<div class="data-table-row fleet-containers-table-grid border-b border-neutral-200 last:border-b-0 dark:border-white/[0.07]">
<div class="min-w-0 truncate text-[13px] font-medium text-black dark:text-fg">
@if ($c['link'])
<a href="{{ $c['link'] }}" class="hover:underline" {{ wireNavigate() }}>{{ $c['name'] }}</a>
<div class="flex min-w-0 flex-col">
@if ($meta['link'])
<a href="{{ $meta['link'] }}" class="truncate text-[13px] font-medium text-black hover:underline dark:text-fg" {{ wireNavigate() }}>{{ $meta['name'] }}</a>
@else
{{ $c['name'] }}
<span class="truncate text-[13px] font-medium text-black dark:text-fg">{{ $meta['name'] }}</span>
@endif
@if ($meta['image'])
<span class="truncate font-mono text-[11px] text-neutral-400 dark:text-fg-faint">{{ $meta['image'] }}</span>
@endif
</div>
<div class="min-w-0 truncate text-[12px] text-neutral-500 dark:text-fg-dim">{{ $c['server'] }}</div>
<div class="text-right text-[13px] tabular-nums text-black dark:text-fg">
<div class="min-w-0 self-center truncate text-[12px] text-neutral-500 dark:text-fg-dim">{{ $c['server'] }}</div>
<div class="self-center text-right text-[13px] tabular-nums text-black dark:text-fg">
@switch($containerMetric)
@case('memory'){{ formatBytes($c['memUsed'] ?? 0) }}@break
@case('disk'){{ formatBytes($c['diskBytes'] ?? 0) }}@break
@@ -223,6 +233,9 @@ $serverListboxOptions = array_merge(
@script
<script>
(() => {
// Defer to the next frame so the wire:ignore chart containers are laid out
// before ApexCharts measures them (a zero-size container renders blank).
requestAnimationFrame(() => {
if (typeof ApexCharts === 'undefined') { return; }
if (typeof checkTheme === 'function') { checkTheme(); }
const axisColor = (typeof textColor !== 'undefined') ? textColor : 'rgba(128,128,128,0.7)';
@@ -282,6 +295,7 @@ $serverListboxOptions = array_merge(
chart.updateSeries(next[key].series);
}
});
});
})();
</script>
@endscript