Files
coolify/app/Livewire/Project/Service/Heading.php
T
Aditya Tripathi af093eea6e feat(service): reopen the live deployment log after closing it
The deploy/start log lived in a fire-and-forget dialog; closing it left the
deploy running server-side with no way back, and re-clicking Deploy only
errored 'deployment in progress'.

- Track the running activity id in the service heading (refreshed on the 10s
  poll, on broadcasts, and instantly when a deploy starts)
- Add reopenDeployment(): re-attach the activity monitor to the running deploy
  and reopen the dialog
- Show a persistent 'Deploying… View log' indicator (mobile status row +
  desktop action HUD) that reopens the live log
- Clicking Deploy/Restart while a deploy runs now reopens the log instead of
  throwing an error
- New reusable x-deploying-indicator component
2026-09-11 08:00:02 +00:00

286 lines
9.0 KiB
PHP

<?php
namespace App\Livewire\Project\Service;
use App\Actions\Docker\GetContainersStatus;
use App\Actions\Service\StartService;
use App\Actions\Service\StopService;
use App\Actions\Service\StopServiceApplication;
use App\Enums\ProcessStatus;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use Spatie\Activitylog\Models\Activity;
class Heading extends Component
{
use AuthorizesRequests;
public Service $service;
public array $parameters;
public array $query;
public $isDeploymentProgress = false;
public $runningActivityId = null;
public $docker_cleanup = true;
public $title = 'Configuration';
public function mount()
{
$this->authorizeService('view');
$this->checkDeployments();
if (str($this->service->status)->contains('running') && is_null($this->service->config_hash)) {
$this->service->isConfigurationChanged(true);
$this->dispatch('configurationChanged');
}
}
public function getListeners()
{
$teamId = Auth::user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ServiceStatusChanged" => 'checkStatus',
"echo-private:team.{$teamId},ServiceChecked" => 'serviceChecked',
'refresh' => '$refresh',
'envsUpdated' => '$refresh',
];
}
public function checkStatus()
{
$this->authorizeService('view');
$this->checkDeployments();
if ($this->service->server->isFunctional()) {
GetContainersStatus::dispatch($this->service->server);
} else {
$this->dispatch('error', 'Server is not functional.');
}
}
public function manualCheckStatus()
{
$this->checkStatus();
}
public function serviceChecked()
{
$this->authorizeService('view');
try {
$this->service->applications->each(function ($application) {
$application->refresh();
});
$this->service->databases->each(function ($database) {
$database->refresh();
});
if (is_null($this->service->config_hash)) {
$this->service->isConfigurationChanged(true);
}
$this->dispatch('configurationChanged');
} catch (\Exception $e) {
return handleError($e, $this);
} finally {
$this->dispatch('refresh')->self();
}
}
public function checkDeployments()
{
$this->authorizeService('view');
try {
$activity = Activity::where('properties->type_uuid', $this->service->uuid)->latest()->first();
$status = data_get($activity, 'properties.status');
if ($status === ProcessStatus::QUEUED->value || $status === ProcessStatus::IN_PROGRESS->value) {
$this->isDeploymentProgress = true;
$this->runningActivityId = $activity->id;
} else {
$this->isDeploymentProgress = false;
$this->runningActivityId = null;
}
} catch (\Throwable) {
$this->isDeploymentProgress = false;
$this->runningActivityId = null;
}
return $this->isDeploymentProgress;
}
/**
* Re-attach the live log dialog to a deployment that is already running.
* Used by the "Deploying…" indicator and when Deploy/Restart is clicked
* while a deployment is in progress, so the running log reappears instead
* of a dead-end error.
*/
public function reopenDeployment()
{
$this->authorizeService('view');
$this->checkDeployments();
if ($this->isDeploymentProgress && $this->runningActivityId) {
$this->dispatch('activityMonitor', $this->runningActivityId);
$this->js("window.dispatchEvent(new CustomEvent('startservice'))");
} else {
$this->dispatch('info', 'No deployment is currently running.');
}
}
public function start()
{
try {
$this->authorizeService('deploy');
$activity = StartService::run($this->service, pullLatestImages: true);
$this->auditServiceAction('ui.service.started');
$this->markDeploymentRunning($activity->id);
$this->js("window.dispatchEvent(new CustomEvent('startservice'))");
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function forceDeploy()
{
try {
$this->authorizeService('deploy');
$activities = Activity::where('properties->type_uuid', $this->service->uuid)
->where(function ($q) {
$q->where('properties->status', ProcessStatus::IN_PROGRESS->value)
->orWhere('properties->status', ProcessStatus::QUEUED->value);
})->get();
foreach ($activities as $activity) {
$activity->properties->status = ProcessStatus::ERROR->value;
$activity->save();
}
$activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true);
$this->markDeploymentRunning($activity->id);
$this->js("window.dispatchEvent(new CustomEvent('startservice'))");
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
private function markDeploymentRunning($activityId): void
{
$this->isDeploymentProgress = true;
$this->runningActivityId = $activityId;
}
public function stop()
{
try {
$this->authorizeService('stop');
StopService::dispatch($this->service, false, $this->docker_cleanup);
$this->auditServiceAction('ui.service.stopped');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function restart()
{
try {
$this->authorizeService('deploy');
$this->checkDeployments();
if ($this->isDeploymentProgress) {
$this->dispatch('error', 'There is a deployment in progress.');
return;
}
$activity = StartService::run($this->service, stopBeforeStart: true);
$this->auditServiceAction('ui.service.restarted');
$this->markDeploymentRunning($activity->id);
$this->js("window.dispatchEvent(new CustomEvent('startservice'))");
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function removeSelectedResourceContainer(): void
{
$resource = $this->selectedResource();
if (! $resource) {
return;
}
$this->authorize('update', $resource);
StopServiceApplication::run($resource, true, true);
$this->dispatch('success', 'Container removed.');
}
private function selectedResource(): ServiceApplication|ServiceDatabase|null
{
$uuid = data_get($this->parameters, 'stack_service_uuid');
if (! $uuid) {
return null;
}
return $this->service->applications()->whereUuid($uuid)->first()
?? $this->service->databases()->whereUuid($uuid)->first();
}
public function pullAndRestartEvent()
{
try {
$this->authorizeService('deploy');
$this->checkDeployments();
if ($this->isDeploymentProgress) {
$this->dispatch('error', 'There is a deployment in progress.');
return;
}
$activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true);
$this->auditServiceAction('ui.service.restarted');
$this->markDeploymentRunning($activity->id);
$this->js("window.dispatchEvent(new CustomEvent('startservice'))");
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
private function authorizeService(string $ability): void
{
$this->service = Service::ownedByCurrentTeam()
->whereKey($this->service->getKey())
->firstOrFail();
$this->authorize($ability, $this->service);
}
private function auditServiceAction(string $event): void
{
auditLog($event, [
'team_id' => $this->service->team()?->id,
'service_uuid' => $this->service->uuid,
'service_name' => $this->service->name,
]);
}
public function render()
{
return view('livewire.project.service.heading', [
'checkboxes' => [
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
],
]);
}
}