feat(ui): consolidate resource heading actions

This commit is contained in:
Andras Bacsai committed 2026-08-13 14:08:33 +02:00
1 parent 5f3108b68d
commit 1932673c22
6 files changed
+312 -111

No files matched your search

+7
View File
@@ -1809,6 +1809,13 @@ html[data-theme="custom"] textarea:disabled {
color: var(--color-fg);
}
.application-heading-actions .button-highlighted,
.dark .application-heading-actions .button-highlighted,
.application-heading-actions .button-highlighted:hover:not(:disabled),
.dark .application-heading-actions .button-highlighted:hover:not(:disabled) {
@apply button-highlighted;
}
/*
* Active primary tab styles.
* The base rules above set background/color/box-shadow with higher specificity
@@ -42,7 +42,6 @@
<button type="button" class="button w-full justify-between" @click="open = !open"
:aria-expanded="open" aria-haspopup="menu">
<span class="inline-flex items-center gap-2">
<x-reicon name="play-circle" class="size-3.5 text-warning" />
Actions
</span>
<span class="inline-flex transition-transform" :class="open && 'rotate-180'">
@@ -186,37 +185,100 @@
<div class="resource-heading-menus shrink-0">
<x-applications.links :application="$application" />
</div>
<x-applications.deploy :application="$application" />
<x-resource-heading-overflow id="application-desktop-actions">
@if (! str($application->status)->startsWith('exited'))
@if ($application->build_pack !== 'dockercompose' && ! $application->destination->server->isSwarm())
<div id="application-desktop-actions" class="relative" x-data="{ open: false }"
x-effect="$dispatch('resource-actions-toggled', { open })"
@click.outside="open = false" @keydown.escape.window="open = false">
<button type="button" class="button button-highlighted" @click="open = !open" :aria-expanded="open"
aria-haspopup="menu">
Actions
<x-reicon name="chevron-down" class="size-3 opacity-55" />
</button>
<div x-cloak x-show="open" x-transition.origin.top.right
class="listbox-panel top-full! right-0! left-auto! mt-1! w-60! min-w-0!" role="menu">
@if (str($application->status)->startsWith('exited'))
<button type="button" class="listbox-option justify-start! gap-2.5!"
@disabled(!auth()->user()->can('deploy', $application))
wire:click="deploy" @click="open = false" role="menuitem">
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
Deploy
</button>
@if (!$application->destination->server->isSwarm())
<button type="button" class="listbox-option justify-start! gap-2.5!"
@disabled(!auth()->user()->can('deploy', $application))
wire:click="deploy(true)" @click="open = false" role="menuitem">
<x-reicon name="refresh" class="size-3.5 opacity-70" />
Deploy (without cache)
</button>
@endif
@else
@if (!$application->destination->server->isSwarm())
@can('deploy', $application)
<button type="button" class="listbox-option justify-start! gap-2.5!"
wire:click="deploy" @click="open = false" role="menuitem">
<x-reicon name="refresh" class="size-3.5 opacity-70" />
Redeploy
</button>
@else
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled>
<x-reicon name="refresh" class="size-3.5 opacity-70" />
Redeploy
</button>
@endcan
<button type="button" class="listbox-option justify-start! gap-2.5!"
@disabled(!auth()->user()->can('deploy', $application))
wire:click="{{ str($application->status)->startsWith('running') ? 'force_deploy_without_cache' : 'deploy(true)' }}"
@click="open = false" role="menuitem">
<x-reicon name="refresh" class="size-3.5 opacity-70" />
{{ str($application->status)->startsWith('running') ? 'Redeploy (without cache)' : 'Deploy (without cache)' }}
</button>
@endif
@if ($application->build_pack !== 'dockercompose')
@if ($application->destination->server->isSwarm())
@can('deploy', $application)
<button type="button" class="listbox-option justify-start! gap-2.5!"
wire:click="deploy" @click="open = false" role="menuitem">
<x-reicon name="refresh" class="size-3.5 opacity-70" />
Update Service
</button>
@else
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled>
<x-reicon name="refresh" class="size-3.5 opacity-70" />
Update Service
</button>
@endcan
@else
@can('deploy', $application)
<button type="button" class="listbox-option justify-start! gap-2.5!"
@click="open = false; document.getElementById('application-mobile-restart-trigger')?.click()"
role="menuitem">
<x-reicon name="restart" class="size-3.5 opacity-70" />
Restart
</button>
@else
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled>
<x-reicon name="restart" class="size-3.5 opacity-70" />
Restart
</button>
@endcan
@endif
@endif
@can('deploy', $application)
<button type="button" class="button"
@click="open = false; document.getElementById('application-mobile-restart-trigger')?.click()">
<x-reicon name="restart" class="size-3.5 opacity-70" />
Restart
<button type="button" class="listbox-option justify-start! gap-2.5!"
@click="open = false; document.getElementById('application-mobile-stop-trigger')?.click()"
role="menuitem">
<x-reicon name="stop-circle" class="size-3.5 text-error" />
Stop
</button>
@else
<button type="button" class="button" disabled>
<x-reicon name="restart" class="size-3.5 opacity-70" />
Restart
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled>
<x-reicon name="stop-circle" class="size-3.5 opacity-70" />
Stop
</button>
@endcan
@endif
@can('deploy', $application)
<button type="button" class="button"
@click="open = false; document.getElementById('application-mobile-stop-trigger')?.click()">
<x-reicon name="stop-circle" class="size-3.5 text-error" />
Stop
</button>
@else
<button type="button" class="button" disabled>
<x-reicon name="stop-circle" class="size-3.5 opacity-70" />
Stop
</button>
@endcan
@endif
</x-resource-heading-overflow>
</div>
</div>
@endif
</div>
</div>
@@ -75,7 +75,6 @@
<button type="button" class="button w-full justify-between" @click="open = !open"
:aria-expanded="open" aria-haspopup="menu">
<span class="inline-flex items-center gap-2">
<x-reicon name="play-circle" class="size-3.5 text-warning" />
Actions
</span>
<span class="inline-flex transition-transform" :class="open && 'rotate-180'">
@@ -138,28 +137,25 @@
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 ($database->destination->server->isFunctional())
<x-resource-heading-overflow id="database-desktop-actions">
<div id="database-desktop-actions" class="flex items-center gap-0.5">
@if (! $databaseStatus->startsWith('exited'))
<button type="button" class="button"
<button type="button" class="button button-highlighted"
@disabled(!auth()->user()->can('manage', $database))
@click="open = false; document.getElementById('database-restart-trigger')?.click()">
<x-reicon name="restart" class="size-3.5 opacity-70" />
@click="document.getElementById('database-restart-trigger')?.click()">
Restart
</button>
<button type="button" class="button"
@disabled(!auth()->user()->can('manage', $database))
@click="open = false; document.getElementById('database-stop-trigger')?.click()">
<x-reicon name="stop" class="size-3.5 text-error" />
@click="document.getElementById('database-stop-trigger')?.click()">
Stop
</button>
@else
<x-forms.button canGate="manage" :canResource="$database"
<x-forms.button class="button-highlighted" canGate="manage" :canResource="$database"
@click="$wire.dispatch('startEvent')">
<x-reicon name="play-circle" class="size-4 opacity-70" />
Start
</x-forms.button>
@endif
</x-resource-heading-overflow>
</div>
@else
<x-status-badge status="Server unavailable" type="error" />
@endif
@@ -78,7 +78,6 @@
: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'">
@@ -184,25 +183,70 @@
<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
<div id="service-desktop-actions" class="relative" x-data="{ open: false }"
x-effect="$dispatch('resource-actions-toggled', { open })"
@click.outside="open = false" @keydown.escape.window="open = false">
<button type="button" class="button button-highlighted" @click="open = !open" :aria-expanded="open">
Actions
<x-reicon name="chevron-down" class="size-3 opacity-55" />
</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>
<div x-cloak x-show="open" x-transition.origin.top.right
class="listbox-panel top-full! right-0! left-auto! mt-1! w-64! min-w-0!" role="menu">
@if ($serviceStatus->contains('running') || $serviceStatus->contains('degraded'))
<button type="button" class="listbox-option justify-start! gap-2.5!"
@disabled(!auth()->user()->can('deploy', $service))
@click="open = false; document.getElementById('service-restart-trigger')?.click()">
<x-reicon name="restart" class="size-3.5 opacity-70" />
Restart
</button>
@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">
<x-reicon name="refresh" class="size-3.5 opacity-70" />
Restart (pull latest)
</button>
@endif
<button type="button" class="listbox-option justify-start! gap-2.5!"
@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>
@elseif (! $serviceStatus->contains('running'))
<button type="button" class="listbox-option justify-start! gap-2.5!"
@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
@if (! $serviceStatus->contains('running'))
<div class="my-1 border-t border-coolgray-200 dark:border-coolgray-300" role="separator"></div>
@endif
@if ($serviceStatus->contains('degraded'))
<button type="button" class="listbox-option justify-start! gap-2.5!"
@disabled(!auth()->user()->can('deploy', $service))
@click="$wire.dispatch('forceDeployEvent'); open = false">
<x-reicon name="refresh" class="size-3.5 opacity-70" />
Force Restart
</button>
@elseif (! $serviceStatus->contains('running'))
<button type="button" class="listbox-option justify-start! gap-2.5!"
@disabled(!auth()->user()->can('deploy', $service))
@click="$wire.dispatch('forceDeployEvent'); open = false">
<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">
<x-reicon name="trash" class="size-3.5 opacity-70" />
Force Cleanup Containers
</button>
@endif
</div>
</div>
@else
<a href="{{ $environmentVariablesUrl }}" {{ wireNavigate() }}
aria-label="Open required environment variables">
@@ -157,7 +157,6 @@
<button type="button" class="button w-full justify-between" @click="open = !open"
:aria-expanded="open" aria-haspopup="menu">
<span class="inline-flex items-center gap-2">
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
Actions
</span>
<span class="inline-flex transition-transform" :class="open && 'rotate-180'">
@@ -286,33 +285,66 @@
@if ($server->proxySet())
@can('manageProxy', $server)
<x-server.advanced :server-ip="$serverIp" :traefik-dashboard-available="$traefikDashboardAvailable" />
<x-resource-heading-overflow id="server-desktop-actions" class="resource-heading-actions">
@if ($proxyCanBeStopped)
<button type="button" class="button"
@click="open = false; document.getElementById('server-mobile-restart-proxy-trigger')?.click()">
<div id="server-desktop-actions" class="resource-heading-actions relative shrink-0"
x-data="{ open: false }" x-effect="$dispatch('resource-actions-toggled', { open })"
@click.outside="open = false"
@keydown.escape.window="open = false">
<button type="button" class="button button-highlighted" @click="open = !open" :aria-expanded="open"
aria-haspopup="menu" wire:loading.attr="disabled" wire:loading.class="is-loading"
wire:target="checkProxy,startProxy">
Actions
<x-reicon name="chevron-down" class="size-3 opacity-55" />
</button>
<div x-cloak x-show="open" x-transition.origin.top.right
class="listbox-panel top-full! right-0! left-auto! mt-1! w-60! min-w-0!" role="menu">
@if ($proxyCanBeStopped)
<button type="button" class="listbox-option justify-start! gap-2.5!"
@click="open = false; document.getElementById('server-mobile-restart-proxy-trigger')?.click()"
role="menuitem">
<span class="flex size-4 shrink-0 items-center justify-center">
<x-reicon name="restart" class="size-3.5 opacity-70" />
</span>
Restart Proxy
</button>
<button type="button" class="listbox-option justify-start! gap-2.5!"
@click="open = false; document.getElementById('server-mobile-stop-proxy-trigger')?.click()"
role="menuitem">
<span class="flex size-4 shrink-0 items-center justify-center">
<x-reicon name="stop-circle" class="size-3.5 text-error" />
</span>
Stop Proxy
</button>
@else
<button type="button" class="listbox-option justify-start! gap-2.5!"
@click="open = false; $wire.dispatch('checkProxyEvent')" role="menuitem">
<span class="flex size-4 shrink-0 items-center justify-center">
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
</span>
Start Proxy
</button>
@endif
<div class="my-1 border-t border-coolgray-200 dark:border-coolgray-300"
role="separator"></div>
<button type="button" class="listbox-option justify-start! gap-2.5!"
wire:click="checkProxyStatus" wire:loading.attr="disabled"
@click="open = false" role="menuitem">
<span class="flex size-4 shrink-0 items-center justify-center">
<x-reicon name="restart" class="size-3.5 opacity-70" />
<x-reicon name="refresh" class="size-3.5 opacity-70" />
</span>
Restart Proxy
Refresh Proxy Status
</button>
<button type="button" class="button"
@click="open = false; document.getElementById('server-mobile-stop-proxy-trigger')?.click()">
<span class="flex size-4 shrink-0 items-center justify-center">
<x-reicon name="stop-circle" class="size-3.5 text-error" />
</span>
Stop Proxy
</button>
@else
<button type="button" class="button"
@click="open = false; $wire.dispatch('checkProxyEvent')">
<span class="flex size-4 shrink-0 items-center justify-center">
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
</span>
Start Proxy
</button>
@endif
</x-resource-heading-overflow>
@if ($traefikDashboardAvailable)
<a class="listbox-option justify-start! gap-2.5!" target="_blank"
href="http://{{ $serverIp }}:8080" @click="open = false" role="menuitem">
<span class="flex size-4 shrink-0 items-center justify-center">
<x-reicon name="external-link" class="size-3! opacity-70" />
</span>
Traefik Dashboard
</a>
@endif
</div>
</div>
@endcan
@endif
</div>