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
This commit is contained in:
Aditya Tripathi
2026-09-11 08:00:02 +00:00
parent c046b186aa
commit af093eea6e
3 changed files with 68 additions and 4 deletions
+39
View File
@@ -27,6 +27,8 @@ class Heading extends Component
public $isDeploymentProgress = false;
public $runningActivityId = null;
public $docker_cleanup = true;
public $title = 'Configuration';
@@ -35,6 +37,8 @@ class Heading extends Component
{
$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');
@@ -57,6 +61,8 @@ class Heading extends Component
{
$this->authorizeService('view');
$this->checkDeployments();
if ($this->service->server->isFunctional()) {
GetContainersStatus::dispatch($this->service->server);
} else {
@@ -101,22 +107,46 @@ class Heading extends Component
$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) {
@@ -138,6 +168,7 @@ class Heading extends Component
$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) {
@@ -145,6 +176,12 @@ class Heading extends Component
}
}
private function markDeploymentRunning($activityId): void
{
$this->isDeploymentProgress = true;
$this->runningActivityId = $activityId;
}
public function stop()
{
try {
@@ -168,6 +205,7 @@ class Heading extends Component
}
$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) {
@@ -210,6 +248,7 @@ class Heading extends Component
}
$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) {
@@ -0,0 +1,19 @@
@props([
'action' => 'reopenDeployment',
'label' => 'Deploying',
])
{{-- Persistent affordance shown while a deploy/start is running. Re-opens the
live log dialog (the process runs server-side even after the modal closes).
Uses the Alpine $wire magic (not wire:click) so it also works when the
button is teleported to the desktop action HUD, outside the Livewire root. --}}
<button type="button" x-on:click="$wire.{{ $action }}()"
{{ $attributes->class('inline-flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1 text-[11px] font-medium ring-1 transition-colors bg-coollabs/10 text-coollabs ring-coollabs/25 hover:bg-coollabs/15 dark:bg-warning/15 dark:text-warning dark:ring-warning/25 dark:hover:bg-warning/20') }}
title="View the running deployment log">
<svg class="size-3 animate-spin" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2.5" opacity="0.25" />
<path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" />
</svg>
<span>{{ $label }}…</span>
<span class="opacity-70">View log</span>
</button>
@@ -75,6 +75,9 @@
<div class="relative flex w-full min-w-0 items-center gap-2">
<x-status-summary :status="$service->status" title="Service status" container-name="Containers" />
<x-services.links :service="$service" compact />
@if ($isDeploymentProgress)
<x-deploying-indicator />
@endif
</div>
<div class="flex w-full flex-wrap gap-1">
@if ($selectedResource)
@@ -173,6 +176,9 @@
<div
class="resource-heading-navbar application-heading-actions flex w-auto min-w-0 items-center justify-end gap-1 overflow-visible">
<div class="resource-heading-actions flex shrink-0 items-center gap-0.5">
@if ($isDeploymentProgress)
<x-deploying-indicator class="mr-1" />
@endif
@if ($service->isDeployable)
<div class="resource-heading-menus shrink-0">
<x-services.links :service="$service" />
@@ -303,8 +309,8 @@
const isDeploymentProgress = await $wire.$call('checkDeployments');
if (isDeploymentProgress) {
$wire.$dispatch('error',
'There is a deployment in progress.<br><br>You can force deploy from the Actions menu.');
// A deploy is already running: reopen its live log instead of erroring.
$wire.$call('reopenDeployment');
return;
}
@@ -317,8 +323,8 @@
const isDeploymentProgress = await $wire.$call('checkDeployments');
if (isDeploymentProgress) {
$wire.$dispatch('error',
'There is a deployment in progress.<br><br>You can force deploy from the Actions menu.');
// A deploy is already running: reopen its live log instead of erroring.
$wire.$call('reopenDeployment');
return;
}