feat(services): show template update timestamps

This commit is contained in:
Andras Bacsai
2026-06-04 09:34:11 +02:00
parent 2833c68af9
commit 981b670eb4
3 changed files with 143 additions and 3 deletions
+56 -1
View File
@@ -4,7 +4,9 @@ namespace App\Livewire\Project\New;
use App\Models\Project; use App\Models\Project;
use App\Models\Server; use App\Models\Server;
use Carbon\CarbonImmutable;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Livewire\Component; use Livewire\Component;
class Select extends Component class Select extends Component
@@ -105,7 +107,9 @@ class Select extends Component
public function loadServices() public function loadServices()
{ {
$services = get_service_templates(); $services = get_service_templates();
$services = collect($services)->map(function ($service, $key) { $templateLastUpdatedMap = $this->serviceTemplateLastUpdatedMap($services->keys());
$services = collect($services)->map(function ($service, $key) use ($templateLastUpdatedMap) {
$default_logo = 'images/default.webp'; $default_logo = 'images/default.webp';
$logo = data_get($service, 'logo', $default_logo); $logo = data_get($service, 'logo', $default_logo);
$local_logo_path = public_path($logo); $local_logo_path = public_path($logo);
@@ -116,6 +120,7 @@ class Select extends Component
'logo_github_url' => file_exists($local_logo_path) 'logo_github_url' => file_exists($local_logo_path)
? 'https://raw.githubusercontent.com/coollabsio/coolify/refs/heads/main/public/'.$logo ? 'https://raw.githubusercontent.com/coollabsio/coolify/refs/heads/main/public/'.$logo
: asset($default_logo), : asset($default_logo),
'templateLastUpdated' => $templateLastUpdatedMap[(string) $key] ?? null,
] + (array) $service; ] + (array) $service;
})->all(); })->all();
@@ -247,6 +252,7 @@ class Select extends Component
]; ];
return [ return [
'serviceTemplatesLastUpdated' => $this->serviceTemplatesLastUpdated(),
'services' => $services, 'services' => $services,
'categories' => $categories, 'categories' => $categories,
'gitBasedApplications' => $gitBasedApplications, 'gitBasedApplications' => $gitBasedApplications,
@@ -268,6 +274,55 @@ class Select extends Component
} }
} }
private function serviceTemplatesLastUpdated(): ?string
{
return $this->formatLastModified($this->serviceTemplatesPath());
}
private function serviceTemplateLastUpdatedMap(Collection $serviceNames): array
{
$bundleMtime = file_exists($this->serviceTemplatesPath()) ? filemtime($this->serviceTemplatesPath()) : 0;
return Cache::remember(
"service-template-last-updated-map:{$bundleMtime}",
now()->addDay(),
fn () => $serviceNames
->mapWithKeys(fn ($serviceName) => [
(string) $serviceName => $this->serviceTemplateLastUpdated((string) $serviceName),
])
->all()
);
}
private function serviceTemplateLastUpdated(string $serviceName): ?string
{
foreach (['yaml', 'yml'] as $extension) {
$templatePath = base_path("templates/compose/{$serviceName}.{$extension}");
if (file_exists($templatePath)) {
return $this->formatLastModified($templatePath);
}
}
return null;
}
private function serviceTemplatesPath(): string
{
return base_path('templates/'.config('constants.services.file_name'));
}
private function formatLastModified(string $path): ?string
{
if (! file_exists($path)) {
return null;
}
return CarbonImmutable::createFromTimestamp(filemtime($path))
->timezone(config('app.timezone'))
->format('M j, Y H:i');
}
public function setType(string $type) public function setType(string $type)
{ {
$type = str($type)->lower()->slug()->value(); $type = str($type)->lower()->slug()->value();
@@ -138,9 +138,14 @@
</div> </div>
</div> </div>
<div x-show="filteredServices.length > 0" class="mt-8"> <div x-show="filteredServices.length > 0" class="mt-8">
<div class="flex items-center gap-4" x-init="loadResources"> <div class="flex flex-wrap items-center gap-4" x-init="loadResources">
<h2>Services</h2> <h2>Services</h2>
<x-forms.button x-on:click="loadResources">Reload List</x-forms.button> <x-forms.button x-on:click="loadResources">Reload List</x-forms.button>
<div x-show="serviceTemplatesLastUpdated"
class="text-xs text-neutral-500 dark:text-neutral-400">
Last Updated on Service Templates:
<span x-text="serviceTemplatesLastUpdated"></span>
</div>
</div> </div>
<x-callout type="info" title="Trademarks Policy" class="mt-4 mb-6"> <x-callout type="info" title="Trademarks Policy" class="mt-4 mb-6">
The respective trademarks mentioned here are owned by the respective companies, and use of them The respective trademarks mentioned here are owned by the respective companies, and use of them
@@ -154,7 +159,14 @@
<x-resource-view> <x-resource-view>
<x-slot:title> <x-slot:title>
<template x-if="service.name"> <template x-if="service.name">
<span x-text="service.name"></span> <div>
<span x-text="service.name"></span>
<template x-if="service.templateLastUpdated">
<div class="mt-1 text-[0.7rem] font-normal text-neutral-500 dark:text-neutral-500">
Updated: <span x-text="service.templateLastUpdated"></span>
</div>
</template>
</div>
</template> </template>
</x-slot> </x-slot>
<x-slot:description> <x-slot:description>
@@ -237,6 +249,7 @@
isSticky: false, isSticky: false,
selecting: false, selecting: false,
services: [], services: [],
serviceTemplatesLastUpdated: null,
gitBasedApplications: [], gitBasedApplications: [],
dockerBasedApplications: [], dockerBasedApplications: [],
databases: [], databases: [],
@@ -251,12 +264,14 @@
this.loading = true; this.loading = true;
const { const {
services, services,
serviceTemplatesLastUpdated,
categories, categories,
gitBasedApplications, gitBasedApplications,
dockerBasedApplications, dockerBasedApplications,
databases databases
} = await this.$wire.loadServices(); } = await this.$wire.loadServices();
this.services = services; this.services = services;
this.serviceTemplatesLastUpdated = serviceTemplatesLastUpdated;
this.categories = categories || []; this.categories = categories || [];
this.gitBasedApplications = gitBasedApplications; this.gitBasedApplications = gitBasedApplications;
this.dockerBasedApplications = dockerBasedApplications; this.dockerBasedApplications = dockerBasedApplications;
@@ -0,0 +1,70 @@
<?php
use App\Livewire\Project\New\Select;
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ViewErrorBag;
beforeEach(function () {
Cache::flush();
});
it('returns the service templates bundle last updated timestamp', function () {
$component = new Select;
$templatePath = base_path('templates/'.config('constants.services.file_name'));
$resources = $component->loadServices();
expect($resources)
->toHaveKey('serviceTemplatesLastUpdated')
->and($resources['serviceTemplatesLastUpdated'])
->toBe(CarbonImmutable::createFromTimestamp(filemtime($templatePath))->timezone(config('app.timezone'))->format('M j, Y H:i'));
});
it('returns each service template last updated timestamp', function () {
$component = new Select;
$templatePath = base_path('templates/compose/activepieces.yaml');
$resources = $component->loadServices();
expect($resources['services']['activepieces'])
->toHaveKey('templateLastUpdated')
->and($resources['services']['activepieces']['templateLastUpdated'])
->toBe(CarbonImmutable::createFromTimestamp(filemtime($templatePath))->timezone(config('app.timezone'))->format('M j, Y H:i'));
});
it('uses a service template timestamp cache keyed by bundle mtime', function () {
$bundleMtime = filemtime(base_path('templates/'.config('constants.services.file_name')));
Cache::put("service-template-last-updated-map:{$bundleMtime}", [
'activepieces' => 'Cached timestamp',
], now()->addDay());
$resources = (new Select)->loadServices();
expect($resources['services']['activepieces']['templateLastUpdated'])->toBe('Cached timestamp');
});
it('does not use stale service template timestamp cache entries from another bundle mtime', function () {
$bundleMtime = filemtime(base_path('templates/'.config('constants.services.file_name')));
Cache::put('service-template-last-updated-map:'.($bundleMtime - 1), [
'activepieces' => 'Stale cached timestamp',
], now()->addDay());
$resources = (new Select)->loadServices();
expect($resources['services']['activepieces']['templateLastUpdated'])->not->toBe('Stale cached timestamp');
});
it('renders the service templates last updated hint placeholder', function () {
View::share('errors', new ViewErrorBag);
$view = $this->view('livewire.project.new.select', [
'current_step' => 'type',
'environments' => collect(),
]);
$view->assertSee('Last Updated on Service Templates:');
$view->assertSee('serviceTemplatesLastUpdated');
$view->assertSee('service.templateLastUpdated');
});