From 981b670eb4e816c020016c3bd20a55a17c8c1540 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 4 Jun 2026 09:34:11 +0200 Subject: [PATCH] feat(services): show template update timestamps --- app/Livewire/Project/New/Select.php | 57 ++++++++++++++- .../livewire/project/new/select.blade.php | 19 ++++- .../ServiceTemplatesLastUpdatedHintTest.php | 70 +++++++++++++++++++ 3 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 tests/Feature/ServiceTemplatesLastUpdatedHintTest.php diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php index 165e4b59e..d6d234b18 100644 --- a/app/Livewire/Project/New/Select.php +++ b/app/Livewire/Project/New/Select.php @@ -4,7 +4,9 @@ namespace App\Livewire\Project\New; use App\Models\Project; use App\Models\Server; +use Carbon\CarbonImmutable; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Cache; use Livewire\Component; class Select extends Component @@ -105,7 +107,9 @@ class Select extends Component public function loadServices() { $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'; $logo = data_get($service, 'logo', $default_logo); $local_logo_path = public_path($logo); @@ -116,6 +120,7 @@ class Select extends Component 'logo_github_url' => file_exists($local_logo_path) ? 'https://raw.githubusercontent.com/coollabsio/coolify/refs/heads/main/public/'.$logo : asset($default_logo), + 'templateLastUpdated' => $templateLastUpdatedMap[(string) $key] ?? null, ] + (array) $service; })->all(); @@ -247,6 +252,7 @@ class Select extends Component ]; return [ + 'serviceTemplatesLastUpdated' => $this->serviceTemplatesLastUpdated(), 'services' => $services, 'categories' => $categories, '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) { $type = str($type)->lower()->slug()->value(); diff --git a/resources/views/livewire/project/new/select.blade.php b/resources/views/livewire/project/new/select.blade.php index c5482d9f7..debe3326f 100644 --- a/resources/views/livewire/project/new/select.blade.php +++ b/resources/views/livewire/project/new/select.blade.php @@ -138,9 +138,14 @@
-
+

Services

Reload List +
+ Last Updated on Service Templates: + +
The respective trademarks mentioned here are owned by the respective companies, and use of them @@ -154,7 +159,14 @@ @@ -237,6 +249,7 @@ isSticky: false, selecting: false, services: [], + serviceTemplatesLastUpdated: null, gitBasedApplications: [], dockerBasedApplications: [], databases: [], @@ -251,12 +264,14 @@ this.loading = true; const { services, + serviceTemplatesLastUpdated, categories, gitBasedApplications, dockerBasedApplications, databases } = await this.$wire.loadServices(); this.services = services; + this.serviceTemplatesLastUpdated = serviceTemplatesLastUpdated; this.categories = categories || []; this.gitBasedApplications = gitBasedApplications; this.dockerBasedApplications = dockerBasedApplications; diff --git a/tests/Feature/ServiceTemplatesLastUpdatedHintTest.php b/tests/Feature/ServiceTemplatesLastUpdatedHintTest.php new file mode 100644 index 000000000..99b9f7ad7 --- /dev/null +++ b/tests/Feature/ServiceTemplatesLastUpdatedHintTest.php @@ -0,0 +1,70 @@ +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'); +});