coolify/resources/views/livewire/project/service/heading.blade.php
Andras Bacsai 5f3108b68d feat(ui): unify resource headings with responsive action menus
Add responsive overflow handling and dedicated action menus for application,
service, database, and server headings, while improving upgrade and email
settings flows and returning 404s for missing deployment environments.
2026-08-13 12:17:16 +02:00

286 lines
15 KiB
PHP

<nav wire:poll.10000ms="checkStatus" class="w-full max-w-[1180px] pb-4 md:pb-6 lg:pb-0">
@php
$servicePageItems = [
[
'label' => 'Settings',
'route' => 'project.service.configuration',
'active' => request()->routeIs('project.service.configuration')
|| request()->routeIs('project.service.domains')
|| request()->routeIs('project.service.environment-variables')
|| request()->routeIs('project.service.storages')
|| request()->routeIs('project.service.scheduled-tasks*')
|| request()->routeIs('project.service.webhooks')
|| request()->routeIs('project.service.resource-operations')
|| request()->routeIs('project.service.tags')
|| request()->routeIs('project.service.danger')
|| request()->routeIs('project.service.index*')
|| request()->routeIs('project.service.database.*'),
],
[
'label' => 'Backups',
'route' => 'project.service.volume-backups.index',
'active' => request()->routeIs('project.service.volume-backups.*'),
],
[
'label' => 'Runtime Logs',
'route' => 'project.service.logs',
'active' => request()->routeIs('project.service.logs'),
'navigate' => false,
],
[
'label' => 'Terminal',
'route' => 'project.service.command',
'active' => request()->routeIs('project.service.command'),
'navigate' => false,
'visible' => auth()->user()?->can('canAccessTerminal'),
],
];
$servicePageItems = array_values(array_filter(
$servicePageItems,
fn (array $item): bool => $item['visible'] ?? true,
));
$serviceStatus = str($service->status ?? 'exited');
$environmentVariablesUrl = route('project.service.environment-variables', [
'project_uuid' => $service->environment->project->uuid,
'environment_uuid' => $service->environment->uuid,
'service_uuid' => $service->uuid,
]);
@endphp
<livewire:project.shared.configuration-checker :resource="$service" />
<x-process-dialog @startservice.window="processDialogOpen = true" closeWithX>
<x-slot:title>Service Startup</x-slot:title>
<x-slot:content>
<livewire:activity-monitor header="Logs" fullHeight />
</x-slot:content>
</x-process-dialog>
<div x-data="{ deploying: false }" @service-deploy-finished.window="deploying = false">
<div class="mb-3 w-full xl:hidden">
<div class="flex min-w-0 flex-col items-start gap-2">
<h1 class="min-w-0 max-w-full truncate text-[24px]! leading-7! font-semibold! tracking-tight! text-black dark:text-fg">
{{ $service->name }}
</h1>
<x-status-summary :status="$service->status" title="Service status" container-name="Containers" />
</div>
</div>
<div class="w-full xl:hidden">
@if ($service->isDeployable)
<div id="service-mobile-actions" class="relative mb-3"
x-data="{ open: false }" @click.outside="open = false"
@keydown.escape.window="open = false">
<button type="button" class="button w-full justify-between" x-bind:disabled="deploying"
@click="open = !open"
:aria-expanded="open" aria-haspopup="menu">
<span class="inline-flex items-center gap-2">
<x-loading-on-button x-show="deploying" x-cloak />
<x-reicon name="play-circle" class="size-3.5 text-warning" x-show="!deploying" />
<span x-text="deploying ? 'Deploying…' : 'Actions'">Actions</span>
</span>
<span class="inline-flex transition-transform" :class="open && 'rotate-180'">
<x-reicon name="chevron-down" class="size-3 opacity-55" />
</span>
</button>
<div x-cloak x-show="open" x-transition.origin.top.left
class="listbox-panel top-full! left-0! right-0! mt-1! w-full! min-w-0!" role="menu">
@if ($serviceStatus->contains('running') || $serviceStatus->contains('degraded'))
@can('deploy', $service)
<button type="button" class="listbox-option justify-start! gap-2.5!"
@click="open = false; document.getElementById('service-restart-trigger')?.click()"
role="menuitem">
<x-reicon name="restart" class="size-3.5 opacity-70" />
Restart current version
</button>
@else
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled
role="menuitem">
<x-reicon name="restart" class="size-3.5 opacity-70" />
Restart current version
</button>
@endcan
@if ($serviceStatus->contains('running'))
<button type="button" class="listbox-option justify-start! gap-2.5!"
@disabled(!auth()->user()->can('deploy', $service))
@click="$wire.dispatch('pullAndRestartEvent'); open = false"
role="menuitem">
<x-reicon name="refresh" class="size-3.5 opacity-70" />
Pull latest and restart
</button>
@else
<button type="button" class="listbox-option justify-start! gap-2.5!"
@disabled(!auth()->user()->can('deploy', $service))
@click="$wire.dispatch('forceDeployEvent'); open = false"
role="menuitem">
<x-reicon name="refresh" class="size-3.5 opacity-70" />
Force Restart
</button>
@endif
@can('stop', $service)
<button type="button" class="listbox-option justify-start! gap-2.5!"
@click="open = false; document.getElementById('service-stop-trigger')?.click()"
role="menuitem">
<x-reicon name="stop-circle" class="size-3.5 text-error" />
Stop
</button>
@else
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled
role="menuitem">
<x-reicon name="stop-circle" class="size-3.5 opacity-70" />
Stop
</button>
@endcan
@else
@can('deploy', $service)
<button type="button" class="listbox-option justify-start! gap-2.5!"
@click="open = false; deploying = true; $wire.dispatch('startEvent')" role="menuitem">
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
Deploy
</button>
@else
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled
role="menuitem">
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
Deploy
</button>
@endcan
<button type="button" class="listbox-option justify-start! gap-2.5!"
@disabled(!auth()->user()->can('deploy', $service))
@click="$wire.dispatch('forceDeployEvent'); open = false" role="menuitem">
<x-reicon name="refresh" class="size-3.5 opacity-70" />
Force Deploy
</button>
<button type="button" class="listbox-option justify-start! gap-2.5!"
@disabled(!auth()->user()->can('stop', $service))
@click="$wire.dispatch('cleanupEvent'); open = false" role="menuitem">
<x-reicon name="trash" class="size-3.5 opacity-70" />
Force Cleanup Containers
</button>
@endif
</div>
</div>
@else
<a href="{{ $environmentVariablesUrl }}" {{ wireNavigate() }}
class="mb-3 inline-flex" aria-label="Open required environment variables">
<x-status-badge status="Required variables missing" type="error" />
</a>
@endif
<div class="resource-heading-menus w-full">
<x-services.links :service="$service" full-width />
</div>
</div>
@teleport('#resource-action-hud-slot')
<div class="hidden w-full items-center xl:flex xl:w-auto">
<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 ($service->isDeployable)
<div class="resource-heading-menus shrink-0">
<x-services.links :service="$service" />
</div>
<x-services.advanced :service="$service" />
<x-services.restart :service="$service" />
<x-resource-heading-overflow id="service-desktop-actions">
@if ($serviceStatus->contains('running') || $serviceStatus->contains('degraded'))
<button type="button" class="button"
@disabled(!auth()->user()->can('stop', $service))
@click="open = false; document.getElementById('service-stop-trigger')?.click()">
<x-reicon name="stop-circle" class="size-3.5 text-error" />
Stop
</button>
@else
<button type="button" class="button"
@disabled(!auth()->user()->can('deploy', $service))
@click="deploying = true; $wire.dispatch('startEvent'); open = false">
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
Deploy
</button>
@endif
</x-resource-heading-overflow>
@else
<a href="{{ $environmentVariablesUrl }}" {{ wireNavigate() }}
aria-label="Open required environment variables">
<x-status-badge status="Required variables missing" type="error" />
</a>
@endif
</div>
</div>
</div>
@endteleport
</div>
@if ($service->isDeployable)
<div class="hidden" aria-hidden="true">
<x-modal-confirmation title="Confirm Service Restart?" buttonTitle="Restart"
submitAction="restartEvent" :dispatchAction="true" :actions="['This service will be restarted.']"
:confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Confirm">
<x-slot:trigger>
<button id="service-restart-trigger" type="button">Restart</button>
</x-slot:trigger>
</x-modal-confirmation>
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop"
submitAction="stop" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue"
step2ButtonText="Confirm">
<x-slot:trigger>
<button id="service-stop-trigger" type="button">Stop</button>
</x-slot:trigger>
</x-modal-confirmation>
</div>
@endif
@script
<script>
$wire.$on('stopEvent', () => {
$wire.$dispatch('info',
'Gracefully stopping service.<br/><br/>It could take a while depending on the service.');
$wire.$call('stop');
});
$wire.$on('startEvent', async () => {
try {
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.');
return;
}
await $wire.$call('start');
} finally {
window.dispatchEvent(new CustomEvent('service-deploy-finished'));
}
});
$wire.$on('restartEvent', async () => {
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.');
return;
}
$wire.$dispatch('info',
'Gracefully stopping service.<br/><br/>It could take a while depending on the service.');
$wire.$call('restart');
});
$wire.$on('forceDeployEvent', () => $wire.$call('forceDeploy'));
$wire.$on('pullAndRestartEvent', () => {
$wire.$dispatch('info', 'Pulling new images and restarting service.');
$wire.$call('pullAndRestartEvent');
});
$wire.$on('cleanupEvent', () => $wire.$call('stop', true));
$wire.on('imagePulled', () => {
window.dispatchEvent(new CustomEvent('startservice'));
$wire.$dispatch('info', 'Restarting service.');
});
</script>
@endscript
</nav>