mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-12 15:53:46 -04:00
fix(metrics): rAF chart render, container image resolution, tab loading state, hi-fi skeleton
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user