feat(resources): add responsive mobile navigation menus
Unify application, database, and service headings with mobile-friendly resource menus, route-aware configuration tabs, and confirmation triggers for advanced actions. Standardize restart warnings with status badges and cover the responsive menu and badge rendering behavior with feature tests.
This commit is contained in:
parent
51583069f4
commit
a9d9bfdbe3
15 changed files with 1238 additions and 414 deletions
|
|
@ -3,34 +3,50 @@
|
|||
Advanced
|
||||
</x-slot>
|
||||
@if ($application->status === 'running')
|
||||
<div class="dropdown-iteme" wire:click='force_deploy_without_cache'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12.983 8.978c3.955 -.182 7.017 -1.446 7.017 -2.978c0 -1.657 -3.582 -3 -8 -3c-1.661 0 -3.204 .19 -4.483 .515m-2.783 1.228c-.471 .382 -.734 .808 -.734 1.257c0 1.22 1.944 2.271 4.734 2.74" />
|
||||
<path
|
||||
d="M4 6v6c0 1.657 3.582 3 8 3c.986 0 1.93 -.067 2.802 -.19m3.187 -.82c1.251 -.53 2.011 -1.228 2.011 -1.99v-6" />
|
||||
<path d="M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4" />
|
||||
<path d="M3 3l18 18" />
|
||||
</svg>
|
||||
Force deploy (without
|
||||
cache)
|
||||
</div>
|
||||
<x-modal-confirmation title="Confirm Application Force Deployment?" buttonTitle="Force deploy"
|
||||
submitAction="force_deploy_without_cache" :actions="[
|
||||
'This application will be force deployed without build cache.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step2ButtonText="Confirm">
|
||||
<x-slot:content>
|
||||
<div class="dropdown-iteme">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12.983 8.978c3.955 -.182 7.017 -1.446 7.017 -2.978c0 -1.657 -3.582 -3 -8 -3c-1.661 0 -3.204 .19 -4.483 .515m-2.783 1.228c-.471 .382 -.734 .808 -.734 1.257c0 1.22 1.944 2.271 4.734 2.74" />
|
||||
<path
|
||||
d="M4 6v6c0 1.657 3.582 3 8 3c.986 0 1.93 -.067 2.802 -.19m3.187 -.82c1.251 -.53 2.011 -1.228 2.011 -1.99v-6" />
|
||||
<path d="M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4" />
|
||||
<path d="M3 3l18 18" />
|
||||
</svg>
|
||||
Force deploy (without
|
||||
cache)
|
||||
</div>
|
||||
</x-slot:content>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<div class="dropdown-item" wire:click='deploy(true)'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12.983 8.978c3.955 -.182 7.017 -1.446 7.017 -2.978c0 -1.657 -3.582 -3 -8 -3c-1.661 0 -3.204 .19 -4.483 .515m-2.783 1.228c-.471 .382 -.734 .808 -.734 1.257c0 1.22 1.944 2.271 4.734 2.74" />
|
||||
<path
|
||||
d="M4 6v6c0 1.657 3.582 3 8 3c.986 0 1.93 -.067 2.802 -.19m3.187 -.82c1.251 -.53 2.011 -1.228 2.011 -1.99v-6" />
|
||||
<path d="M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4" />
|
||||
<path d="M3 3l18 18" />
|
||||
</svg>
|
||||
Force deploy (without
|
||||
cache)
|
||||
</div>
|
||||
<x-modal-confirmation title="Confirm Application Force Deployment?" buttonTitle="Force deploy"
|
||||
submitAction="deploy(true)" :actions="[
|
||||
'This application will be force deployed without build cache.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step2ButtonText="Confirm">
|
||||
<x-slot:content>
|
||||
<div class="dropdown-item">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12.983 8.978c3.955 -.182 7.017 -1.446 7.017 -2.978c0 -1.657 -3.582 -3 -8 -3c-1.661 0 -3.204 .19 -4.483 .515m-2.783 1.228c-.471 .382 -.734 .808 -.734 1.257c0 1.22 1.944 2.271 4.734 2.74" />
|
||||
<path
|
||||
d="M4 6v6c0 1.657 3.582 3 8 3c.986 0 1.93 -.067 2.802 -.19m3.187 -.82c1.251 -.53 2.011 -1.228 2.011 -1.99v-6" />
|
||||
<path d="M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4" />
|
||||
<path d="M3 3l18 18" />
|
||||
</svg>
|
||||
Force deploy (without
|
||||
cache)
|
||||
</div>
|
||||
</x-slot:content>
|
||||
</x-modal-confirmation>
|
||||
@endif
|
||||
</x-dropdown>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
'isImportSupported' => false,
|
||||
])
|
||||
|
||||
<div class="sub-menu-wrapper">
|
||||
<div class="sub-menu-wrapper hidden md:flex">
|
||||
<a class="sub-menu-item"
|
||||
class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-active' : '' }}"
|
||||
{{ wireNavigate() }}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Advanced
|
||||
</x-slot>
|
||||
@if (str($service->status)->contains('running'))
|
||||
<div class="dropdown-item" @click="$wire.dispatch('pullAndRestartEvent')">
|
||||
<div class="dropdown-item" @click="document.getElementById('service-pullAndRestart-trigger')?.click()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
Pull Latest Images & Restart
|
||||
</div>
|
||||
@elseif (str($service->status)->contains('degraded'))
|
||||
<div class="dropdown-item" @click="$wire.dispatch('forceDeployEvent')">
|
||||
<div class="dropdown-item" @click="document.getElementById('service-forceDeploy-trigger')?.click()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke=""
|
||||
style="--darkreader-inline-stroke: currentColor;" class="w-6 h-6" stroke-width="2">
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
Force Restart
|
||||
</div>
|
||||
@else
|
||||
<div class="dropdown-item" @click="$wire.dispatch('forceDeployEvent')">
|
||||
<div class="dropdown-item" @click="document.getElementById('service-forceDeploy-trigger')?.click()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke=""
|
||||
style="--darkreader-inline-stroke: currentColor;" class="w-4 h-4" stroke-width="2">
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
</svg>
|
||||
Force Deploy
|
||||
</div>
|
||||
<div class="dropdown-item" wire:click='stop(true)''>
|
||||
<div class="dropdown-item" @click="document.getElementById('service-cleanup-trigger')?.click()">
|
||||
<svg class="w-4 h-4" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor" d="M26 20h-6v-2h6zm4 8h-6v-2h6zm-2-4h-6v-2h6z" />
|
||||
<path fill="currentColor"
|
||||
|
|
|
|||
|
|
@ -18,18 +18,13 @@
|
|||
<x-status.stopped :status="$resource->status" />
|
||||
@endif
|
||||
@if (isset($resource->restart_count) && $resource->restart_count > 0 && (!str($resource->status)->startsWith('exited') || $stoppedAfterRestartLimit))
|
||||
<div class="flex items-center">
|
||||
<span class="text-xs dark:text-warning" title="Container has restarted {{ $resource->restart_count }} time{{ $resource->restart_count > 1 ? 's' : '' }}. Last restart: {{ $resource->last_restart_at?->diffForHumans() }}">
|
||||
({{ $resource->restart_count }}x restarts)
|
||||
</span>
|
||||
</div>
|
||||
<x-status-badge status="{{ $resource->restart_count }}x restarts" type="warning"
|
||||
title="Container has restarted {{ $resource->restart_count }} time{{ $resource->restart_count > 1 ? 's' : '' }}. Last restart: {{ $resource->last_restart_at?->diffForHumans() }}" />
|
||||
@endif
|
||||
@if ($stoppedAfterRestartLimit)
|
||||
<div class="flex items-center">
|
||||
<span class="text-xs dark:text-warning" title="Container has crashed and Coolify stopped it after {{ $resource->restart_count }} restart attempts.">
|
||||
Stopped after reaching restart limit ({{ $resource->restart_count }}/{{ $resource->max_restart_count }}).
|
||||
</span>
|
||||
</div>
|
||||
<x-status-badge status="Stopped after reaching restart limit ({{ $resource->restart_count }}/{{ $resource->max_restart_count }})."
|
||||
type="warning"
|
||||
title="Container has crashed and Coolify stopped it after {{ $resource->restart_count }} restart attempts." />
|
||||
@endif
|
||||
@if (!str($resource->status)->contains('exited') && $showRefreshButton)
|
||||
<x-status-badge as="button" wire:target="manualCheckStatus" wire:loading.attr="disabled"
|
||||
|
|
|
|||
|
|
@ -6,55 +6,128 @@
|
|||
<livewire:project.shared.configuration-checker :resource="$application" />
|
||||
<livewire:project.application.heading :application="$application" />
|
||||
|
||||
<div class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<div class="sub-menu-wrapper">
|
||||
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">General</span></a>
|
||||
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.advanced', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Advanced</span></a>
|
||||
@if ($application->destination->server->isSwarm())
|
||||
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.swarm', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Swarm</span>
|
||||
@php
|
||||
$applicationRouteParameters = [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'application_uuid' => $application->uuid,
|
||||
];
|
||||
|
||||
$configurationMenuItems = [
|
||||
[
|
||||
'label' => 'General',
|
||||
'route' => 'project.application.configuration',
|
||||
'active' => $currentRoute === 'project.application.configuration',
|
||||
],
|
||||
[
|
||||
'label' => 'Advanced',
|
||||
'route' => 'project.application.advanced',
|
||||
'active' => $currentRoute === 'project.application.advanced',
|
||||
],
|
||||
[
|
||||
'label' => 'Swarm',
|
||||
'route' => 'project.application.swarm',
|
||||
'active' => $currentRoute === 'project.application.swarm',
|
||||
'visible' => $application->destination->server->isSwarm(),
|
||||
],
|
||||
[
|
||||
'label' => 'Environment Variables',
|
||||
'route' => 'project.application.environment-variables',
|
||||
'active' => $currentRoute === 'project.application.environment-variables',
|
||||
],
|
||||
[
|
||||
'label' => 'Persistent Storage',
|
||||
'route' => 'project.application.persistent-storage',
|
||||
'active' => $currentRoute === 'project.application.persistent-storage',
|
||||
],
|
||||
[
|
||||
'label' => 'Git Source',
|
||||
'route' => 'project.application.source',
|
||||
'active' => $currentRoute === 'project.application.source',
|
||||
'visible' => $application->git_based(),
|
||||
],
|
||||
[
|
||||
'label' => 'Servers',
|
||||
'route' => 'project.application.servers',
|
||||
'active' => $currentRoute === 'project.application.servers',
|
||||
'badge' => true,
|
||||
],
|
||||
[
|
||||
'label' => 'Scheduled Tasks',
|
||||
'route' => 'project.application.scheduled-tasks.show',
|
||||
'active' => str($currentRoute)->startsWith('project.application.scheduled-tasks'),
|
||||
],
|
||||
[
|
||||
'label' => 'Webhooks',
|
||||
'route' => 'project.application.webhooks',
|
||||
'active' => $currentRoute === 'project.application.webhooks',
|
||||
],
|
||||
[
|
||||
'label' => 'Preview Deployments',
|
||||
'route' => 'project.application.preview-deployments',
|
||||
'active' => $currentRoute === 'project.application.preview-deployments',
|
||||
'visible' => $application->git_based() || $application->build_pack === 'dockerimage',
|
||||
],
|
||||
[
|
||||
'label' => 'Healthcheck',
|
||||
'route' => 'project.application.healthcheck',
|
||||
'active' => $currentRoute === 'project.application.healthcheck',
|
||||
'visible' => $application->build_pack !== 'dockercompose',
|
||||
],
|
||||
[
|
||||
'label' => 'Rollback',
|
||||
'route' => 'project.application.rollback',
|
||||
'active' => $currentRoute === 'project.application.rollback',
|
||||
],
|
||||
[
|
||||
'label' => 'Resource Limits',
|
||||
'route' => 'project.application.resource-limits',
|
||||
'active' => $currentRoute === 'project.application.resource-limits',
|
||||
],
|
||||
[
|
||||
'label' => 'Resource Operations',
|
||||
'route' => 'project.application.resource-operations',
|
||||
'active' => $currentRoute === 'project.application.resource-operations',
|
||||
],
|
||||
[
|
||||
'label' => 'Metrics',
|
||||
'route' => 'project.application.metrics',
|
||||
'active' => $currentRoute === 'project.application.metrics',
|
||||
],
|
||||
[
|
||||
'label' => 'Tags',
|
||||
'route' => 'project.application.tags',
|
||||
'active' => $currentRoute === 'project.application.tags',
|
||||
],
|
||||
[
|
||||
'label' => 'Danger Zone',
|
||||
'route' => 'project.application.danger',
|
||||
'active' => $currentRoute === 'project.application.danger',
|
||||
],
|
||||
];
|
||||
|
||||
$configurationMenuItems = array_values(array_filter(
|
||||
$configurationMenuItems,
|
||||
fn (array $item): bool => $item['visible'] ?? true,
|
||||
));
|
||||
@endphp
|
||||
|
||||
<div class="flex flex-col h-full gap-4 md:gap-8 md:flex-row">
|
||||
<div class="sub-menu-wrapper hidden md:flex">
|
||||
@foreach ($configurationMenuItems as $menuItem)
|
||||
<a @class([
|
||||
'sub-menu-item',
|
||||
'flex items-center gap-2' => $menuItem['badge'] ?? false,
|
||||
'menu-item-active' => $menuItem['active'],
|
||||
]) {{ wireNavigate() }} href="{{ route($menuItem['route'], $applicationRouteParameters) }}">
|
||||
<span class="menu-item-label">{{ $menuItem['label'] }}</span>
|
||||
@if ($menuItem['badge'] ?? false)
|
||||
<livewire:project.application.server-status-badge :application="$application" />
|
||||
@endif
|
||||
</a>
|
||||
@endif
|
||||
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.environment-variables', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Environment Variables</span></a>
|
||||
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.persistent-storage', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Persistent Storage</span></a>
|
||||
@if ($application->git_based())
|
||||
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.source', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Git Source</span></a>
|
||||
@endif
|
||||
<a class="sub-menu-item flex items-center gap-2" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.servers', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Servers</span>
|
||||
<livewire:project.application.server-status-badge :application="$application" />
|
||||
</a>
|
||||
<a @class(['sub-menu-item', 'menu-item-active' => str($currentRoute)->startsWith('project.application.scheduled-tasks')]) {{ wireNavigate() }}
|
||||
href="{{ route('project.application.scheduled-tasks.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Scheduled Tasks</span></a>
|
||||
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.webhooks', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Webhooks</span></a>
|
||||
@if ($application->git_based() || $application->build_pack === 'dockerimage')
|
||||
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.preview-deployments', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Preview Deployments</span></a>
|
||||
@endif
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.healthcheck', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Healthcheck</span></a>
|
||||
@endif
|
||||
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.rollback', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Rollback</span></a>
|
||||
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.resource-limits', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Resource Limits</span></a>
|
||||
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.resource-operations', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Resource Operations</span></a>
|
||||
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.metrics', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Metrics</span></a>
|
||||
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.tags', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Tags</span></a>
|
||||
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.danger', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Danger Zone</span></a>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="w-full sm:flex-grow">
|
||||
<div class="w-full md:flex-grow">
|
||||
@if ($currentRoute === 'project.application.configuration')
|
||||
<livewire:project.application.general :application="$application" />
|
||||
@elseif ($currentRoute === 'project.application.swarm' && $application->destination->server->isSwarm())
|
||||
|
|
|
|||
|
|
@ -1,17 +1,373 @@
|
|||
<nav wire:poll.10000ms="checkStatus" class="pb-6">
|
||||
@php
|
||||
$applicationMenuItems = [
|
||||
[
|
||||
'label' => 'Configuration',
|
||||
'route' => 'project.application.configuration',
|
||||
'active' => request()->routeIs('project.application.configuration'),
|
||||
],
|
||||
[
|
||||
'label' => 'Deployments',
|
||||
'route' => 'project.application.deployment.index',
|
||||
'active' => request()->routeIs('project.application.deployment.index', 'project.application.deployment.show'),
|
||||
],
|
||||
[
|
||||
'label' => 'Logs',
|
||||
'route' => 'project.application.logs',
|
||||
'active' => request()->routeIs('project.application.logs'),
|
||||
],
|
||||
[
|
||||
'label' => 'Terminal',
|
||||
'route' => 'project.application.command',
|
||||
'active' => request()->routeIs('project.application.command'),
|
||||
'navigate' => false,
|
||||
'visible' => ! $application->destination->server->isSwarm() && auth()->user()?->can('canAccessTerminal'),
|
||||
],
|
||||
];
|
||||
|
||||
$configurationMenuItems = [
|
||||
[
|
||||
'label' => 'General',
|
||||
'route' => 'project.application.configuration',
|
||||
'active' => request()->routeIs('project.application.configuration'),
|
||||
],
|
||||
[
|
||||
'label' => 'Advanced',
|
||||
'route' => 'project.application.advanced',
|
||||
'active' => request()->routeIs('project.application.advanced'),
|
||||
],
|
||||
[
|
||||
'label' => 'Swarm',
|
||||
'route' => 'project.application.swarm',
|
||||
'active' => request()->routeIs('project.application.swarm'),
|
||||
'visible' => $application->destination->server->isSwarm(),
|
||||
],
|
||||
[
|
||||
'label' => 'Environment Variables',
|
||||
'route' => 'project.application.environment-variables',
|
||||
'active' => request()->routeIs('project.application.environment-variables'),
|
||||
],
|
||||
[
|
||||
'label' => 'Persistent Storage',
|
||||
'route' => 'project.application.persistent-storage',
|
||||
'active' => request()->routeIs('project.application.persistent-storage'),
|
||||
],
|
||||
[
|
||||
'label' => 'Git Source',
|
||||
'route' => 'project.application.source',
|
||||
'active' => request()->routeIs('project.application.source'),
|
||||
'visible' => $application->git_based(),
|
||||
],
|
||||
[
|
||||
'label' => 'Servers',
|
||||
'route' => 'project.application.servers',
|
||||
'active' => request()->routeIs('project.application.servers'),
|
||||
],
|
||||
[
|
||||
'label' => 'Scheduled Tasks',
|
||||
'route' => 'project.application.scheduled-tasks.show',
|
||||
'active' => request()->routeIs('project.application.scheduled-tasks.show', 'project.application.scheduled-tasks'),
|
||||
],
|
||||
[
|
||||
'label' => 'Webhooks',
|
||||
'route' => 'project.application.webhooks',
|
||||
'active' => request()->routeIs('project.application.webhooks'),
|
||||
],
|
||||
[
|
||||
'label' => 'Preview Deployments',
|
||||
'route' => 'project.application.preview-deployments',
|
||||
'active' => request()->routeIs('project.application.preview-deployments'),
|
||||
'visible' => $application->git_based() || $application->build_pack === 'dockerimage',
|
||||
],
|
||||
[
|
||||
'label' => 'Healthcheck',
|
||||
'route' => 'project.application.healthcheck',
|
||||
'active' => request()->routeIs('project.application.healthcheck'),
|
||||
'visible' => $application->build_pack !== 'dockercompose',
|
||||
],
|
||||
[
|
||||
'label' => 'Rollback',
|
||||
'route' => 'project.application.rollback',
|
||||
'active' => request()->routeIs('project.application.rollback'),
|
||||
],
|
||||
[
|
||||
'label' => 'Resource Limits',
|
||||
'route' => 'project.application.resource-limits',
|
||||
'active' => request()->routeIs('project.application.resource-limits'),
|
||||
],
|
||||
[
|
||||
'label' => 'Resource Operations',
|
||||
'route' => 'project.application.resource-operations',
|
||||
'active' => request()->routeIs('project.application.resource-operations'),
|
||||
],
|
||||
[
|
||||
'label' => 'Metrics',
|
||||
'route' => 'project.application.metrics',
|
||||
'active' => request()->routeIs('project.application.metrics'),
|
||||
],
|
||||
[
|
||||
'label' => 'Tags',
|
||||
'route' => 'project.application.tags',
|
||||
'active' => request()->routeIs('project.application.tags'),
|
||||
],
|
||||
[
|
||||
'label' => 'Danger Zone',
|
||||
'route' => 'project.application.danger',
|
||||
'active' => request()->routeIs('project.application.danger'),
|
||||
],
|
||||
];
|
||||
|
||||
$applicationMenuItems = array_values(array_filter(
|
||||
$applicationMenuItems,
|
||||
fn (array $item): bool => $item['visible'] ?? true,
|
||||
));
|
||||
$configurationMenuItems = array_values(array_filter(
|
||||
$configurationMenuItems,
|
||||
fn (array $item): bool => $item['visible'] ?? true,
|
||||
));
|
||||
$activeConfigurationMenuItem = collect($configurationMenuItems)->firstWhere('active', true);
|
||||
$activeApplicationMenuItem = collect($applicationMenuItems)->firstWhere('active', true);
|
||||
$activeMobileMenuItem = $activeConfigurationMenuItem
|
||||
?? $activeApplicationMenuItem
|
||||
?? $applicationMenuItems[0];
|
||||
$activeMobileMenuGroup = $activeConfigurationMenuItem ? 'configuration' : 'application';
|
||||
$activeMobileNavigation = ($activeMobileMenuItem['navigate'] ?? true) ? 'navigate' : 'location';
|
||||
$activeMobileMenuValue = $activeMobileNavigation.'|'.$activeMobileMenuGroup.'|'.route($activeMobileMenuItem['route'], $parameters);
|
||||
$mobileSectionChangeHandler = <<<'JS'
|
||||
const value = $event.target.value;
|
||||
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.startsWith('navigate|')) {
|
||||
const url = value.split('|').slice(2).join('|');
|
||||
window.Livewire?.navigate ? window.Livewire.navigate(url) : window.location.href = url;
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.startsWith('location|')) {
|
||||
const url = value.split('|').slice(2).join('|');
|
||||
window.location.href = url;
|
||||
return;
|
||||
}
|
||||
|
||||
resetToCurrent();
|
||||
|
||||
if (value.startsWith('external:')) {
|
||||
window.open(value.slice(9), '_blank', 'noopener');
|
||||
return;
|
||||
}
|
||||
|
||||
const action = value.slice(7);
|
||||
document.getElementById(`application-mobile-${action}-trigger`)?.click();
|
||||
JS;
|
||||
@endphp
|
||||
<x-resources.breadcrumbs :resource="$application" :parameters="$parameters" :title="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
<div class="navbar-main">
|
||||
<div class="w-full md:hidden">
|
||||
<label for="application-mobile-section" class="sr-only">Application menu</label>
|
||||
<select id="application-mobile-section" class="select w-full" aria-label="Application menu"
|
||||
data-current-value="{{ $activeMobileMenuValue }}"
|
||||
x-data="{
|
||||
init() {
|
||||
this.syncFromLocation();
|
||||
window.Livewire?.hook?.('morphed', ({ el }) => {
|
||||
if (el.contains(this.$el)) {
|
||||
queueMicrotask(() => this.syncFromLocation());
|
||||
}
|
||||
});
|
||||
},
|
||||
selected: $el.dataset.currentValue,
|
||||
current: $el.dataset.currentValue,
|
||||
syncFromLocation() {
|
||||
const currentUrl = new URL(window.location.href);
|
||||
const matchingOptions = Array.from(this.$el.options).filter((option) => {
|
||||
if (!option.value.startsWith('navigate|') && !option.value.startsWith('location|')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const optionUrl = new URL(option.value.split('|').slice(2).join('|'), window.location.origin);
|
||||
|
||||
return optionUrl.pathname === currentUrl.pathname;
|
||||
});
|
||||
const selectedOption = matchingOptions.find((option) => {
|
||||
return option.value.startsWith('navigate|configuration|') || option.value.startsWith('navigate|resource|');
|
||||
}) || matchingOptions[0];
|
||||
|
||||
if (selectedOption) {
|
||||
this.current = selectedOption.value;
|
||||
this.selected = selectedOption.value;
|
||||
}
|
||||
},
|
||||
resetToCurrent() {
|
||||
this.selected = this.current;
|
||||
},
|
||||
}"
|
||||
x-on:livewire:navigated.window="syncFromLocation()"
|
||||
x-model="selected"
|
||||
x-on:change="{{ $mobileSectionChangeHandler }}">
|
||||
<optgroup label="Application">
|
||||
@foreach ($applicationMenuItems as $menuItem)
|
||||
<option value="{{ ($menuItem['navigate'] ?? true) ? 'navigate' : 'location' }}|application|{{ route($menuItem['route'], $parameters) }}">
|
||||
{{ $menuItem['label'] }}
|
||||
</option>
|
||||
@endforeach
|
||||
</optgroup>
|
||||
<optgroup label="Configuration">
|
||||
@foreach ($configurationMenuItems as $menuItem)
|
||||
<option value="navigate|configuration|{{ route($menuItem['route'], $parameters) }}">
|
||||
{{ $menuItem['label'] }}
|
||||
</option>
|
||||
@endforeach
|
||||
</optgroup>
|
||||
<optgroup label="Links">
|
||||
@if (
|
||||
(data_get($application, 'fqdn') ||
|
||||
collect(json_decode($application->docker_compose_domains))->contains(fn($fqdn) => !empty(data_get($fqdn, 'domain'))) ||
|
||||
data_get($application, 'previews', collect([]))->count() > 0 ||
|
||||
data_get($application, 'ports_mappings_array')) &&
|
||||
data_get($application, 'settings.is_raw_compose_deployment_enabled') !== true)
|
||||
@if (data_get($application, 'gitBrancLocation'))
|
||||
<option value="external:{{ $application->gitBranchLocation }}">Git Repository</option>
|
||||
@endif
|
||||
@if (data_get($application, 'build_pack') === 'dockercompose')
|
||||
@foreach (collect(json_decode($application->docker_compose_domains)) as $fqdn)
|
||||
@if (data_get($fqdn, 'domain'))
|
||||
@foreach (explode(',', data_get($fqdn, 'domain')) as $domain)
|
||||
<option value="external:{{ getFqdnWithoutPort($domain) }}">{{ getFqdnWithoutPort($domain) }}</option>
|
||||
@endforeach
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
@if (data_get($application, 'fqdn'))
|
||||
@foreach (str(data_get($application, 'fqdn'))->explode(',') as $fqdn)
|
||||
<option value="external:{{ getFqdnWithoutPort($fqdn) }}">{{ getFqdnWithoutPort($fqdn) }}</option>
|
||||
@endforeach
|
||||
@endif
|
||||
@if (data_get($application, 'previews', collect())->count() > 0)
|
||||
@if (data_get($application, 'build_pack') === 'dockercompose')
|
||||
@foreach ($application->previews as $preview)
|
||||
@foreach (collect(json_decode($preview->docker_compose_domains)) as $fqdn)
|
||||
@if (data_get($fqdn, 'domain'))
|
||||
@foreach (explode(',', data_get($fqdn, 'domain')) as $domain)
|
||||
<option value="external:{{ getFqdnWithoutPort($domain) }}">PR{{ data_get($preview, 'pull_request_id') }} | {{ getFqdnWithoutPort($domain) }}</option>
|
||||
@endforeach
|
||||
@endif
|
||||
@endforeach
|
||||
@endforeach
|
||||
@else
|
||||
@foreach (data_get($application, 'previews') as $preview)
|
||||
@if (data_get($preview, 'fqdn'))
|
||||
<option value="external:{{ getFqdnWithoutPort(data_get($preview, 'fqdn')) }}">PR{{ data_get($preview, 'pull_request_id') }} | {{ data_get($preview, 'fqdn') }}</option>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
@endif
|
||||
@if (data_get($application, 'ports_mappings_array'))
|
||||
@foreach ($application->ports_mappings_array as $port)
|
||||
@if ($application->destination->server->id === 0)
|
||||
<option value="external:http://localhost:{{ explode(':', $port)[0] }}">Port {{ $port }}</option>
|
||||
@else
|
||||
<option value="external:http://{{ $application->destination->server->ip }}:{{ explode(':', $port)[0] }}">{{ $application->destination->server->ip }}:{{ explode(':', $port)[0] }}</option>
|
||||
@if (count($application->additional_servers) > 0)
|
||||
@foreach ($application->additional_servers as $server)
|
||||
<option value="external:http://{{ $server->ip }}:{{ explode(':', $port)[0] }}">{{ $server->ip }}:{{ explode(':', $port)[0] }}</option>
|
||||
@endforeach
|
||||
@endif
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
@else
|
||||
<option disabled>No links available</option>
|
||||
@endif
|
||||
</optgroup>
|
||||
@if (!($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw)))
|
||||
<optgroup label="Actions">
|
||||
@if (!str($application->status)->startsWith('exited'))
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<option value="action:deploy">Redeploy</option>
|
||||
@endif
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
@if ($application->destination->server->isSwarm())
|
||||
<option value="action:deploy">Update Service</option>
|
||||
@else
|
||||
<option value="action:restart">Restart</option>
|
||||
@endif
|
||||
@endif
|
||||
<option value="action:stop">Stop</option>
|
||||
@else
|
||||
<option value="action:deploy">Deploy</option>
|
||||
@endif
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
@if ($application->status === 'running')
|
||||
<option value="action:force-deploy">Force deploy (without cache)</option>
|
||||
@else
|
||||
<option value="action:deploy-force">Force deploy (without cache)</option>
|
||||
@endif
|
||||
@endif
|
||||
</optgroup>
|
||||
@endif
|
||||
</select>
|
||||
<x-modal-confirmation title="Confirm Application Stopping?" buttonTitle="Stop"
|
||||
submitAction="stop" :checkboxes="$checkboxes" :actions="[
|
||||
'This application will be stopped.',
|
||||
'All non-persistent data of this application will be deleted.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<button id="application-mobile-stop-trigger" type="button" class="hidden">Stop</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Application Deployment?" buttonTitle="Deploy"
|
||||
submitAction="deploy" :actions="[
|
||||
'This application will be deployed.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<button id="application-mobile-deploy-trigger" type="button" class="hidden">Deploy</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Application Restart?" buttonTitle="Restart"
|
||||
submitAction="restart" :actions="[
|
||||
'This application will be restarted without rebuilding.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<button id="application-mobile-restart-trigger" type="button" class="hidden">Restart</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Application Force Deployment?" buttonTitle="Force deploy"
|
||||
submitAction="force_deploy_without_cache" :actions="[
|
||||
'This application will be force deployed without build cache.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<button id="application-mobile-force-deploy-trigger" type="button" class="hidden">Force deploy</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Application Force Deployment?" buttonTitle="Force deploy"
|
||||
submitAction="deploy(true)" :actions="[
|
||||
'This application will be force deployed without build cache.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<button id="application-mobile-deploy-force-trigger" type="button" class="hidden">Force deploy</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
</div>
|
||||
|
||||
<nav
|
||||
class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-scroll overflow-y-hidden pb-1 whitespace-nowrap md:w-auto md:overflow-visible">
|
||||
<a class="shrink-0 {{ request()->routeIs('project.application.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
class="scrollbar hidden min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-scroll overflow-y-hidden pb-1 whitespace-nowrap md:flex md:w-auto md:overflow-visible">
|
||||
<a class="hidden md:block shrink-0 {{ request()->routeIs('project.application.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.application.configuration', $parameters) }}">
|
||||
Configuration
|
||||
</a>
|
||||
<a class="shrink-0 {{ request()->routeIs('project.application.deployment.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
<a class="hidden md:block shrink-0 {{ request()->routeIs('project.application.deployment.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.application.deployment.index', $parameters) }}">
|
||||
Deployments
|
||||
</a>
|
||||
<a class="shrink-0 {{ request()->routeIs('project.application.logs') ? 'dark:text-white' : '' }}"
|
||||
<a class="hidden md:block shrink-0 {{ request()->routeIs('project.application.logs') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('project.application.logs', $parameters) }}">
|
||||
<div class="flex items-center gap-1">
|
||||
Logs
|
||||
|
|
@ -24,13 +380,13 @@ class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-
|
|||
</a>
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
@can('canAccessTerminal')
|
||||
<a class="shrink-0 {{ request()->routeIs('project.application.command') ? 'dark:text-white' : '' }}"
|
||||
<a class="hidden md:block shrink-0 {{ request()->routeIs('project.application.command') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('project.application.command', $parameters) }}">
|
||||
Terminal
|
||||
</a>
|
||||
@endcan
|
||||
@endif
|
||||
<div class="shrink-0">
|
||||
<div class="hidden shrink-0 md:block">
|
||||
<x-applications.links :application="$application" />
|
||||
</div>
|
||||
</nav>
|
||||
|
|
@ -38,62 +394,6 @@ class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-
|
|||
@if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
|
||||
<div>Please load a Compose file.</div>
|
||||
@else
|
||||
<div class="md:hidden">
|
||||
<x-dropdown>
|
||||
<x-slot:title>
|
||||
Actions
|
||||
</x-slot>
|
||||
@if (!str($application->status)->startsWith('exited'))
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='deploy'>
|
||||
Redeploy
|
||||
</div>
|
||||
@endif
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
@if ($application->destination->server->isSwarm())
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='deploy'>
|
||||
Update Service
|
||||
</div>
|
||||
@else
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='restart'>
|
||||
Restart
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
<x-modal-confirmation title="Confirm Application Stopping?" buttonTitle="Stop"
|
||||
submitAction="stop" :checkboxes="$checkboxes" :actions="[
|
||||
'This application will be stopped.',
|
||||
'All non-persistent data of this application will be deleted.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<div class="dropdown-item dropdown-item-touch text-error">
|
||||
Stop
|
||||
</div>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='deploy'>
|
||||
Deploy
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<div class="mx-2 my-1 border-t border-neutral-200 dark:border-coolgray-300"></div>
|
||||
|
||||
@if ($application->status === 'running')
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='force_deploy_without_cache'>
|
||||
Force deploy (without cache)
|
||||
</div>
|
||||
@else
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='deploy(true)'>
|
||||
Force deploy (without cache)
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</x-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="hidden flex-wrap items-center gap-2 md:flex">
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<div>
|
||||
|
|
@ -103,46 +403,70 @@ class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-
|
|||
<div class="flex flex-wrap gap-2">
|
||||
@if (!str($application->status)->startsWith('exited'))
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<x-forms.button title="With rolling update if possible" wire:click='deploy'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-orange-400"
|
||||
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
|
||||
</path>
|
||||
<path d="M7.05 11.038v-3.988"></path>
|
||||
</svg>
|
||||
Redeploy
|
||||
</x-forms.button>
|
||||
<x-modal-confirmation title="Confirm Application Deployment?" buttonTitle="Redeploy"
|
||||
submitAction="deploy" :actions="[
|
||||
'This application will be redeployed.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step2ButtonText="Confirm">
|
||||
<x-slot:content>
|
||||
<x-forms.button title="With rolling update if possible">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-orange-400"
|
||||
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
|
||||
</path>
|
||||
<path d="M7.05 11.038v-3.988"></path>
|
||||
</svg>
|
||||
Redeploy
|
||||
</x-forms.button>
|
||||
</x-slot:content>
|
||||
</x-modal-confirmation>
|
||||
@endif
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
@if ($application->destination->server->isSwarm())
|
||||
<x-forms.button title="Redeploy Swarm Service (rolling update)" wire:click='deploy'>
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2">
|
||||
<path
|
||||
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Update Service
|
||||
</x-forms.button>
|
||||
<x-modal-confirmation title="Confirm Application Deployment?" buttonTitle="Update Service"
|
||||
submitAction="deploy" :actions="[
|
||||
'This Swarm service will be updated with a rolling deployment.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step2ButtonText="Confirm">
|
||||
<x-slot:content>
|
||||
<x-forms.button title="Redeploy Swarm Service (rolling update)">
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2">
|
||||
<path
|
||||
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Update Service
|
||||
</x-forms.button>
|
||||
</x-slot:content>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<x-forms.button title="Restart without rebuilding" wire:click='restart'>
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2">
|
||||
<path
|
||||
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Restart
|
||||
</x-forms.button>
|
||||
<x-modal-confirmation title="Confirm Application Restart?" buttonTitle="Restart"
|
||||
submitAction="restart" :actions="[
|
||||
'This application will be restarted without rebuilding.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step2ButtonText="Confirm">
|
||||
<x-slot:content>
|
||||
<x-forms.button title="Restart without rebuilding">
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2">
|
||||
<path
|
||||
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Restart
|
||||
</x-forms.button>
|
||||
</x-slot:content>
|
||||
</x-modal-confirmation>
|
||||
@endif
|
||||
@endif
|
||||
<x-modal-confirmation title="Confirm Application Stopping?" buttonTitle="Stop"
|
||||
|
|
@ -167,15 +491,23 @@ class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-
|
|||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<x-forms.button wire:click='deploy'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning"
|
||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Deploy
|
||||
</x-forms.button>
|
||||
<x-modal-confirmation title="Confirm Application Deployment?" buttonTitle="Deploy"
|
||||
submitAction="deploy" :actions="[
|
||||
'This application will be deployed.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step2ButtonText="Confirm">
|
||||
<x-slot:content>
|
||||
<x-forms.button>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning"
|
||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 4v16l13 -8z" />
|
||||
</svg>
|
||||
Deploy
|
||||
</x-forms.button>
|
||||
</x-slot:content>
|
||||
</x-modal-confirmation>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
<h1>Configuration</h1>
|
||||
<livewire:project.shared.configuration-checker :resource="$database" />
|
||||
<livewire:project.database.heading :database="$database" />
|
||||
<div class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<div class="sub-menu-wrapper">
|
||||
<div class="flex flex-col h-full gap-4 md:gap-8 md:flex-row">
|
||||
<div class="sub-menu-wrapper hidden md:flex">
|
||||
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}"><span class="menu-item-label">General</span></a>
|
||||
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,74 @@
|
|||
<nav wire:poll.10000ms="checkStatus" class="pb-6">
|
||||
@php
|
||||
$databasePageItems = [
|
||||
['label' => 'Configuration', 'route' => 'project.database.configuration', 'active' => request()->routeIs('project.database.configuration')],
|
||||
['label' => 'Logs', 'route' => 'project.database.logs', 'active' => request()->routeIs('project.database.logs')],
|
||||
['label' => 'Terminal', 'route' => 'project.database.command', 'active' => request()->routeIs('project.database.command'), 'navigate' => false, 'visible' => auth()->user()?->can('canAccessTerminal')],
|
||||
[
|
||||
'label' => 'Backups',
|
||||
'route' => 'project.database.backup.index',
|
||||
'active' => request()->routeIs('project.database.backup.index', 'project.database.backup.execution'),
|
||||
'visible' => in_array($database->getMorphClass(), [
|
||||
'App\Models\StandalonePostgresql',
|
||||
'App\Models\StandaloneMongodb',
|
||||
'App\Models\StandaloneMysql',
|
||||
'App\Models\StandaloneMariadb',
|
||||
]),
|
||||
],
|
||||
];
|
||||
|
||||
$databaseConfigurationItems = [
|
||||
['label' => 'General', 'route' => 'project.database.configuration', 'active' => request()->routeIs('project.database.configuration')],
|
||||
['label' => 'Environment Variables', 'route' => 'project.database.environment-variables', 'active' => request()->routeIs('project.database.environment-variables')],
|
||||
['label' => 'Servers', 'route' => 'project.database.servers', 'active' => request()->routeIs('project.database.servers')],
|
||||
['label' => 'Persistent Storage', 'route' => 'project.database.persistent-storage', 'active' => request()->routeIs('project.database.persistent-storage')],
|
||||
['label' => 'Import Backup', 'route' => 'project.database.import-backup', 'active' => request()->routeIs('project.database.import-backup'), 'visible' => auth()->user()?->can('update', $database)],
|
||||
['label' => 'Webhooks', 'route' => 'project.database.webhooks', 'active' => request()->routeIs('project.database.webhooks')],
|
||||
['label' => 'Healthcheck', 'route' => 'project.database.healthcheck', 'active' => request()->routeIs('project.database.healthcheck')],
|
||||
['label' => 'Resource Limits', 'route' => 'project.database.resource-limits', 'active' => request()->routeIs('project.database.resource-limits')],
|
||||
['label' => 'Resource Operations', 'route' => 'project.database.resource-operations', 'active' => request()->routeIs('project.database.resource-operations')],
|
||||
['label' => 'Metrics', 'route' => 'project.database.metrics', 'active' => request()->routeIs('project.database.metrics')],
|
||||
['label' => 'Tags', 'route' => 'project.database.tags', 'active' => request()->routeIs('project.database.tags')],
|
||||
['label' => 'Danger Zone', 'route' => 'project.database.danger', 'active' => request()->routeIs('project.database.danger')],
|
||||
];
|
||||
|
||||
$databasePageItems = array_values(array_filter($databasePageItems, fn (array $item): bool => $item['visible'] ?? true));
|
||||
$databaseConfigurationItems = array_values(array_filter($databaseConfigurationItems, fn (array $item): bool => $item['visible'] ?? true));
|
||||
$activeDatabaseConfigurationItem = collect($databaseConfigurationItems)->firstWhere('active', true);
|
||||
$activeDatabasePageItem = collect($databasePageItems)->firstWhere('active', true);
|
||||
$activeDatabaseMobileItem = $activeDatabaseConfigurationItem ?? $activeDatabasePageItem ?? $databasePageItems[0];
|
||||
$activeDatabaseMobileGroup = $activeDatabaseConfigurationItem ? 'configuration' : 'database';
|
||||
$activeDatabaseMobileNavigation = ($activeDatabaseMobileItem['navigate'] ?? true) ? 'navigate' : 'location';
|
||||
$activeDatabaseMobileValue = $activeDatabaseMobileNavigation.'|'.$activeDatabaseMobileGroup.'|'.route($activeDatabaseMobileItem['route'], $parameters);
|
||||
$databaseMobileMenuChangeHandler = <<<'JS'
|
||||
const value = $event.target.value;
|
||||
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.startsWith('navigate|')) {
|
||||
const url = value.split('|').slice(2).join('|');
|
||||
window.Livewire?.navigate ? window.Livewire.navigate(url) : window.location.href = url;
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.startsWith('location|')) {
|
||||
const url = value.split('|').slice(2).join('|');
|
||||
window.location.href = url;
|
||||
return;
|
||||
}
|
||||
|
||||
resetToCurrent();
|
||||
|
||||
if (value.startsWith('external:')) {
|
||||
window.open(value.slice(9), '_blank', 'noopener');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById(`database-${value.slice(7)}-trigger`)?.click();
|
||||
JS;
|
||||
@endphp
|
||||
<x-resources.breadcrumbs :resource="$database" :parameters="$parameters" />
|
||||
<x-slide-over @startdatabase.window="slideOverOpen = true" closeWithX fullScreen>
|
||||
<x-slot:title>Database Startup</x-slot:title>
|
||||
|
|
@ -9,8 +79,76 @@
|
|||
</x-slot:content>
|
||||
</x-slide-over>
|
||||
<div class="navbar-main">
|
||||
<div class="w-full md:hidden">
|
||||
<label for="database-mobile-section" class="sr-only">Database menu</label>
|
||||
<select id="database-mobile-section" class="select w-full" aria-label="Database menu"
|
||||
data-current-value="{{ $activeDatabaseMobileValue }}"
|
||||
x-data="{
|
||||
init() {
|
||||
this.syncFromLocation();
|
||||
window.Livewire?.hook?.('morphed', ({ el }) => {
|
||||
if (el.contains(this.$el)) {
|
||||
queueMicrotask(() => this.syncFromLocation());
|
||||
}
|
||||
});
|
||||
},
|
||||
selected: $el.dataset.currentValue,
|
||||
current: $el.dataset.currentValue,
|
||||
syncFromLocation() {
|
||||
const currentUrl = new URL(window.location.href);
|
||||
const matchingOptions = Array.from(this.$el.options).filter((option) => {
|
||||
if (!option.value.startsWith('navigate|') && !option.value.startsWith('location|')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const optionUrl = new URL(option.value.split('|').slice(2).join('|'), window.location.origin);
|
||||
|
||||
return optionUrl.pathname === currentUrl.pathname;
|
||||
});
|
||||
const selectedOption = matchingOptions.find((option) => {
|
||||
return option.value.startsWith('navigate|configuration|') || option.value.startsWith('navigate|resource|');
|
||||
}) || matchingOptions[0];
|
||||
|
||||
if (selectedOption) {
|
||||
this.current = selectedOption.value;
|
||||
this.selected = selectedOption.value;
|
||||
}
|
||||
},
|
||||
resetToCurrent() {
|
||||
this.selected = this.current;
|
||||
},
|
||||
}"
|
||||
x-on:livewire:navigated.window="syncFromLocation()"
|
||||
x-model="selected"
|
||||
x-on:change="{{ $databaseMobileMenuChangeHandler }}">
|
||||
<optgroup label="Database">
|
||||
@foreach ($databasePageItems as $menuItem)
|
||||
<option value="{{ ($menuItem['navigate'] ?? true) ? 'navigate' : 'location' }}|database|{{ route($menuItem['route'], $parameters) }}">
|
||||
{{ $menuItem['label'] }}
|
||||
</option>
|
||||
@endforeach
|
||||
</optgroup>
|
||||
<optgroup label="Configuration">
|
||||
@foreach ($databaseConfigurationItems as $menuItem)
|
||||
<option value="navigate|configuration|{{ route($menuItem['route'], $parameters) }}">
|
||||
{{ $menuItem['label'] }}
|
||||
</option>
|
||||
@endforeach
|
||||
</optgroup>
|
||||
@if ($database->destination->server->isFunctional())
|
||||
<optgroup label="Actions">
|
||||
@if (!str($database->status)->startsWith('exited'))
|
||||
<option value="action:restart">Restart</option>
|
||||
<option value="action:stop">Stop</option>
|
||||
@else
|
||||
<option value="action:start">Start</option>
|
||||
@endif
|
||||
</optgroup>
|
||||
@endif
|
||||
</select>
|
||||
</div>
|
||||
<nav
|
||||
class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-scroll overflow-y-hidden pb-1 whitespace-nowrap md:w-auto md:overflow-visible">
|
||||
class="scrollbar hidden min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-scroll overflow-y-hidden pb-1 whitespace-nowrap md:flex md:w-auto md:overflow-visible">
|
||||
<a class="shrink-0 {{ request()->routeIs('project.database.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.database.configuration', $parameters) }}">
|
||||
Configuration
|
||||
|
|
@ -37,89 +175,37 @@ class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-
|
|||
</a>
|
||||
@endif
|
||||
</nav>
|
||||
|
||||
@if ($database->destination->server->isFunctional())
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<div class="md:hidden">
|
||||
<x-dropdown>
|
||||
<x-slot:title>
|
||||
Actions
|
||||
</x-slot>
|
||||
@if (!str($database->status)->startsWith('exited'))
|
||||
<x-modal-confirmation title="Confirm Database Restart?" buttonTitle="Restart" submitAction="restart"
|
||||
:actions="[
|
||||
'This database will be unavailable during the restart.',
|
||||
'If the database is currently in use data could be lost.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Restart Database"
|
||||
:dispatchEvent="true" dispatchEventType="restartEvent">
|
||||
<x-slot:trigger>
|
||||
<div class="dropdown-item dropdown-item-touch">
|
||||
Restart
|
||||
</div>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Database Stopping?" buttonTitle="Stop" submitAction="stop"
|
||||
:checkboxes="$checkboxes" :actions="[
|
||||
'This database will be stopped.',
|
||||
'If the database is currently in use data could be lost.',
|
||||
'All non-persistent data of this database (containers, networks, unused images) will be deleted (don\'t worry, no data is lost and you can start the database again).',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<div class="dropdown-item dropdown-item-touch text-error">
|
||||
Stop
|
||||
</div>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('startEvent')">
|
||||
Start
|
||||
</div>
|
||||
@endif
|
||||
</x-dropdown>
|
||||
</div>
|
||||
<div class="hidden flex-wrap items-center gap-2 md:flex">
|
||||
@if (!str($database->status)->startsWith('exited'))
|
||||
<x-modal-confirmation title="Confirm Database Restart?" buttonTitle="Restart" submitAction="restart"
|
||||
:actions="[
|
||||
'This database will be unavailable during the restart.',
|
||||
'If the database is currently in use data could be lost.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Restart Database"
|
||||
:dispatchEvent="true" dispatchEventType="restartEvent">
|
||||
<x-slot:button-title>
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Restart
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Database Stopping?" buttonTitle="Stop" submitAction="stop"
|
||||
:checkboxes="$checkboxes" :actions="[
|
||||
'This database will be stopped.',
|
||||
'If the database is currently in use data could be lost.',
|
||||
'All non-persistent data of this database (containers, networks, unused images) will be deleted (don\'t worry, no data is lost and you can start the database again).',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
<x-forms.button title="Restart" @click="document.getElementById('database-restart-trigger')?.click()">
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Restart
|
||||
</x-forms.button>
|
||||
<x-forms.button isError title="Stop" @click="document.getElementById('database-stop-trigger')?.click()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
</svg>
|
||||
Stop
|
||||
</x-forms.button>
|
||||
@else
|
||||
<button @click="$wire.dispatch('startEvent')" class="gap-2 button">
|
||||
<button @click="document.getElementById('database-start-trigger')?.click()" class="gap-2 button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
|
|
@ -148,4 +234,37 @@ class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-
|
|||
<div class="text-error">Underlying server is not functional.</div>
|
||||
@endif
|
||||
</div>
|
||||
@if ($database->destination->server->isFunctional())
|
||||
<x-modal-confirmation title="Confirm Database Restart?" buttonTitle="Restart" submitAction="restartEvent"
|
||||
:actions="[
|
||||
'This database will be unavailable during the restart.',
|
||||
'If the database is currently in use data could be lost.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Restart Database"
|
||||
:dispatchAction="true">
|
||||
<x-slot:trigger>
|
||||
<button id="database-restart-trigger" type="button" class="hidden">Restart</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Database Stopping?" buttonTitle="Stop" submitAction="stop"
|
||||
:checkboxes="$checkboxes" :actions="[
|
||||
'This database will be stopped.',
|
||||
'If the database is currently in use data could be lost.',
|
||||
'All non-persistent data of this database (containers, networks, unused images) will be deleted (don\'t worry, no data is lost and you can start the database again).',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<button id="database-stop-trigger" type="button" class="hidden">Stop</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Database Start?" buttonTitle="Start" submitAction="startEvent"
|
||||
:actions="[
|
||||
'This database will be started.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Start Database"
|
||||
:dispatchAction="true">
|
||||
<x-slot:trigger>
|
||||
<button id="database-start-trigger" type="button" class="hidden">Start</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
@endif
|
||||
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
</x-slot>
|
||||
<livewire:project.service.heading :service="$service" :parameters="$parameters" :query="$query" />
|
||||
|
||||
<div class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<div class="sub-menu-wrapper">
|
||||
<div class="flex flex-col h-full gap-4 md:gap-8 md:flex-row">
|
||||
<div class="sub-menu-wrapper hidden md:flex">
|
||||
<a class="sub-menu-item" target="_blank" href="{{ $service->documentation() }}"><span class="menu-item-label">Documentation</span>
|
||||
<x-external-link /></a>
|
||||
<a class='sub-menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<div>
|
||||
<livewire:project.service.heading :service="$service" :parameters="$parameters" :query="$query" />
|
||||
<div class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<div class="flex flex-col h-full gap-4 md:gap-8 md:flex-row">
|
||||
<x-service-database.sidebar :parameters="$parameters" :serviceDatabase="$serviceDatabase" :isImportSupported="$isImportSupported" />
|
||||
<div class="w-full">
|
||||
<x-slot:title>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,94 @@
|
|||
<div wire:poll.10000ms="checkStatus" class="pb-6">
|
||||
@php
|
||||
$servicePageItems = [
|
||||
['label' => 'Configuration', 'route' => 'project.service.configuration', 'active' => request()->routeIs('project.service.configuration')],
|
||||
['label' => 'Logs', 'route' => 'project.service.logs', 'active' => request()->routeIs('project.service.logs')],
|
||||
['label' => 'Terminal', 'route' => 'project.service.command', 'active' => request()->routeIs('project.service.command'), 'navigate' => false, 'visible' => auth()->user()?->can('canAccessTerminal')],
|
||||
];
|
||||
|
||||
$serviceConfigurationItems = [
|
||||
['label' => 'General', 'route' => 'project.service.configuration', 'active' => request()->routeIs('project.service.configuration')],
|
||||
['label' => 'Environment Variables', 'route' => 'project.service.environment-variables', 'active' => request()->routeIs('project.service.environment-variables')],
|
||||
['label' => 'Persistent Storages', 'route' => 'project.service.storages', 'active' => request()->routeIs('project.service.storages')],
|
||||
['label' => 'Scheduled Tasks', 'route' => 'project.service.scheduled-tasks.show', 'active' => request()->routeIs('project.service.scheduled-tasks.show', 'project.service.scheduled-tasks')],
|
||||
['label' => 'Webhooks', 'route' => 'project.service.webhooks', 'active' => request()->routeIs('project.service.webhooks')],
|
||||
['label' => 'Resource Operations', 'route' => 'project.service.resource-operations', 'active' => request()->routeIs('project.service.resource-operations')],
|
||||
['label' => 'Tags', 'route' => 'project.service.tags', 'active' => request()->routeIs('project.service.tags')],
|
||||
['label' => 'Danger Zone', 'route' => 'project.service.danger', 'active' => request()->routeIs('project.service.danger')],
|
||||
];
|
||||
$serviceResourceItems = [];
|
||||
|
||||
if (filled(data_get($parameters, 'stack_service_uuid'))) {
|
||||
$serviceResourceItems = [
|
||||
['label' => 'Back', 'route' => 'project.service.configuration', 'active' => false, 'parameters' => [...$parameters, 'stack_service_uuid' => null]],
|
||||
['label' => 'General', 'route' => 'project.service.index', 'active' => request()->routeIs('project.service.index')],
|
||||
['label' => 'Advanced', 'route' => 'project.service.index.advanced', 'active' => request()->routeIs('project.service.index.advanced')],
|
||||
['label' => 'Backups', 'route' => 'project.service.database.backups', 'active' => request()->routeIs('project.service.database.backups')],
|
||||
['label' => 'Import Backup', 'route' => 'project.service.database.import', 'active' => request()->routeIs('project.service.database.import')],
|
||||
];
|
||||
}
|
||||
|
||||
$servicePageItems = array_values(array_filter($servicePageItems, fn (array $item): bool => $item['visible'] ?? true));
|
||||
$serviceResourceItems = array_values(array_filter($serviceResourceItems, fn (array $item): bool => $item['visible'] ?? true));
|
||||
$activeServiceResourceItem = collect($serviceResourceItems)->firstWhere('active', true);
|
||||
$activeServiceConfigurationItem = collect($serviceConfigurationItems)->firstWhere('active', true);
|
||||
$activeServicePageItem = collect($servicePageItems)->firstWhere('active', true);
|
||||
$activeServiceMobileItem = $activeServiceResourceItem ?? $activeServiceConfigurationItem ?? $activeServicePageItem ?? $servicePageItems[0];
|
||||
$activeServiceMobileGroup = $activeServiceResourceItem ? 'resource' : ($activeServiceConfigurationItem ? 'configuration' : 'service');
|
||||
$activeServiceMobileNavigation = ($activeServiceMobileItem['navigate'] ?? true) ? 'navigate' : 'location';
|
||||
$activeServiceMobileValue = $activeServiceMobileNavigation.'|'.$activeServiceMobileGroup.'|'.route($activeServiceMobileItem['route'], $activeServiceMobileItem['parameters'] ?? $parameters);
|
||||
$serviceLinks = collect([]);
|
||||
$service->applications()->get()->each(function ($application) use ($serviceLinks) {
|
||||
$type = $application->serviceType();
|
||||
if ($type) {
|
||||
generateServiceSpecificFqdns($application)
|
||||
->map(fn ($link) => getFqdnWithoutPort($link))
|
||||
->each(fn ($link) => $serviceLinks->push($link));
|
||||
} else {
|
||||
if ($application->fqdn) {
|
||||
collect(str($application->fqdn)->explode(','))
|
||||
->each(fn ($fqdn) => $serviceLinks->push(getFqdnWithoutPort($fqdn)));
|
||||
}
|
||||
if ($application->ports) {
|
||||
collect(str($application->ports)->explode(','))
|
||||
->each(function ($port) use ($serviceLinks) {
|
||||
$hostPort = str($port)->contains(':') ? str($port)->before(':') : $port;
|
||||
$serviceLinks->push(base_url(withPort: false).":{$hostPort}");
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
$serviceMobileMenuChangeHandler = <<<'JS'
|
||||
const value = $event.target.value;
|
||||
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.startsWith('navigate|')) {
|
||||
const url = value.split('|').slice(2).join('|');
|
||||
window.Livewire?.navigate ? window.Livewire.navigate(url) : window.location.href = url;
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.startsWith('location|')) {
|
||||
const url = value.split('|').slice(2).join('|');
|
||||
window.location.href = url;
|
||||
return;
|
||||
}
|
||||
|
||||
resetToCurrent();
|
||||
|
||||
if (value.startsWith('external:')) {
|
||||
window.open(value.slice(9), '_blank', 'noopener');
|
||||
return;
|
||||
}
|
||||
|
||||
const action = value.slice(7);
|
||||
|
||||
document.getElementById(`service-${action}-trigger`)?.click();
|
||||
JS;
|
||||
@endphp
|
||||
<livewire:project.shared.configuration-checker :resource="$service" />
|
||||
<x-slide-over @startservice.window="slideOverOpen = true" closeWithX fullScreen>
|
||||
<x-slot:title>Service Startup</x-slot:title>
|
||||
|
|
@ -9,8 +99,109 @@
|
|||
<h1>{{ $title }}</h1>
|
||||
<x-resources.breadcrumbs :resource="$service" :parameters="$parameters" />
|
||||
<div class="navbar-main" x-data">
|
||||
<div class="mb-4 w-full md:mb-0 md:hidden">
|
||||
<label for="service-mobile-section" class="sr-only">Service menu</label>
|
||||
<select id="service-mobile-section" class="select w-full" aria-label="Service menu"
|
||||
data-current-value="{{ $activeServiceMobileValue }}"
|
||||
x-data="{
|
||||
init() {
|
||||
this.syncFromLocation();
|
||||
window.Livewire?.hook?.('morphed', ({ el }) => {
|
||||
if (el.contains(this.$el)) {
|
||||
queueMicrotask(() => this.syncFromLocation());
|
||||
}
|
||||
});
|
||||
},
|
||||
selected: $el.dataset.currentValue,
|
||||
current: $el.dataset.currentValue,
|
||||
syncFromLocation() {
|
||||
const currentUrl = new URL(window.location.href);
|
||||
const matchingOptions = Array.from(this.$el.options).filter((option) => {
|
||||
if (!option.value.startsWith('navigate|') && !option.value.startsWith('location|')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const optionUrl = new URL(option.value.split('|').slice(2).join('|'), window.location.origin);
|
||||
|
||||
return optionUrl.pathname === currentUrl.pathname;
|
||||
});
|
||||
const selectedOption = matchingOptions.find((option) => {
|
||||
return option.value.startsWith('navigate|configuration|') || option.value.startsWith('navigate|resource|');
|
||||
}) || matchingOptions[0];
|
||||
|
||||
if (selectedOption) {
|
||||
this.current = selectedOption.value;
|
||||
this.selected = selectedOption.value;
|
||||
}
|
||||
},
|
||||
resetToCurrent() {
|
||||
this.selected = this.current;
|
||||
},
|
||||
}"
|
||||
x-on:livewire:navigated.window="syncFromLocation()"
|
||||
x-model="selected"
|
||||
x-on:change="{{ $serviceMobileMenuChangeHandler }}">
|
||||
<optgroup label="Service">
|
||||
@foreach ($servicePageItems as $menuItem)
|
||||
<option value="{{ ($menuItem['navigate'] ?? true) ? 'navigate' : 'location' }}|service|{{ route($menuItem['route'], $parameters) }}">
|
||||
{{ $menuItem['label'] }}
|
||||
</option>
|
||||
@endforeach
|
||||
</optgroup>
|
||||
<optgroup label="Configuration">
|
||||
@foreach ($serviceConfigurationItems as $menuItem)
|
||||
<option value="navigate|configuration|{{ route($menuItem['route'], $parameters) }}">
|
||||
{{ $menuItem['label'] }}
|
||||
</option>
|
||||
@endforeach
|
||||
</optgroup>
|
||||
@if (count($serviceResourceItems) > 0)
|
||||
<optgroup label="Resource">
|
||||
@foreach ($serviceResourceItems as $menuItem)
|
||||
<option value="navigate|resource|{{ route($menuItem['route'], $menuItem['parameters'] ?? $parameters) }}">
|
||||
{{ $menuItem['label'] }}
|
||||
</option>
|
||||
@endforeach
|
||||
</optgroup>
|
||||
@endif
|
||||
<optgroup label="Links">
|
||||
@if (filled($service->documentation()))
|
||||
<option value="external:{{ $service->documentation() }}">Documentation</option>
|
||||
@endif
|
||||
@forelse ($serviceLinks as $link)
|
||||
<option value="external:{{ $link }}">{{ $link }}</option>
|
||||
@empty
|
||||
@if (blank($service->documentation()))
|
||||
<option disabled>No links available</option>
|
||||
@endif
|
||||
@endforelse
|
||||
</optgroup>
|
||||
@if ($service->isDeployable)
|
||||
<optgroup label="Actions">
|
||||
@if (str($service->status)->contains('running'))
|
||||
<option value="action:restart">Restart</option>
|
||||
<option value="action:stop">Stop</option>
|
||||
<option value="action:pullAndRestart">Pull Latest Images & Restart</option>
|
||||
@elseif (str($service->status)->contains('degraded'))
|
||||
<option value="action:restart">Restart</option>
|
||||
<option value="action:stop">Stop</option>
|
||||
<option value="action:forceDeploy">Force Restart</option>
|
||||
@elseif (str($service->status)->contains('exited'))
|
||||
<option value="action:start">Deploy</option>
|
||||
<option value="action:forceDeploy">Force Deploy</option>
|
||||
<option value="action:cleanup">Force Cleanup Containers</option>
|
||||
@else
|
||||
<option value="action:stop">Stop</option>
|
||||
<option value="action:start">Deploy</option>
|
||||
<option value="action:forceDeploy">Force Deploy</option>
|
||||
<option value="action:cleanup">Force Cleanup Containers</option>
|
||||
@endif
|
||||
</optgroup>
|
||||
@endif
|
||||
</select>
|
||||
</div>
|
||||
<nav
|
||||
class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-scroll overflow-y-hidden pb-1 whitespace-nowrap md:w-auto md:overflow-visible">
|
||||
class="scrollbar hidden min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-scroll overflow-y-hidden pb-1 whitespace-nowrap md:flex md:w-auto md:overflow-visible">
|
||||
<a class="shrink-0 {{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.configuration', $parameters) }}">
|
||||
<button>Configuration</button>
|
||||
|
|
@ -29,85 +220,13 @@ class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-
|
|||
<x-services.links :service="$service" />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@if ($service->isDeployable)
|
||||
<div class="order-first flex flex-wrap items-center gap-2 sm:order-last">
|
||||
<div class="md:hidden">
|
||||
<x-dropdown>
|
||||
<x-slot:title>
|
||||
Actions
|
||||
</x-slot>
|
||||
@if (str($service->status)->contains('running'))
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('restartEvent')">
|
||||
Restart
|
||||
</div>
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<div class="dropdown-item dropdown-item-touch text-error">
|
||||
Stop
|
||||
</div>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<div class="mx-2 my-1 border-t border-neutral-200 dark:border-coolgray-300"></div>
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('pullAndRestartEvent')">
|
||||
Pull Latest Images & Restart
|
||||
</div>
|
||||
@elseif (str($service->status)->contains('degraded'))
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('restartEvent')">
|
||||
Restart
|
||||
</div>
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<div class="dropdown-item dropdown-item-touch text-error">
|
||||
Stop
|
||||
</div>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<div class="mx-2 my-1 border-t border-neutral-200 dark:border-coolgray-300"></div>
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('forceDeployEvent')">
|
||||
Force Restart
|
||||
</div>
|
||||
@elseif (str($service->status)->contains('exited'))
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('startEvent')">
|
||||
Deploy
|
||||
</div>
|
||||
<div class="mx-2 my-1 border-t border-neutral-200 dark:border-coolgray-300"></div>
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('forceDeployEvent')">
|
||||
Force Deploy
|
||||
</div>
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='stop(true)'>
|
||||
Force Cleanup Containers
|
||||
</div>
|
||||
@else
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<div class="dropdown-item dropdown-item-touch text-error">
|
||||
Stop
|
||||
</div>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('startEvent')">
|
||||
Deploy
|
||||
</div>
|
||||
<div class="mx-2 my-1 border-t border-neutral-200 dark:border-coolgray-300"></div>
|
||||
<div class="dropdown-item dropdown-item-touch" @click="$wire.dispatch('forceDeployEvent')">
|
||||
Force Deploy
|
||||
</div>
|
||||
<div class="dropdown-item dropdown-item-touch" wire:click='stop(true)'>
|
||||
Force Cleanup Containers
|
||||
</div>
|
||||
@endif
|
||||
</x-dropdown>
|
||||
</div>
|
||||
<div class="hidden flex-wrap items-center gap-2 md:flex">
|
||||
<div class="hidden flex-wrap items-center gap-2 md:flex">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<x-services.advanced :service="$service" />
|
||||
@if (str($service->status)->contains('running'))
|
||||
<x-forms.button title="Restart" @click="$wire.dispatch('restartEvent')">
|
||||
<x-forms.button title="Restart" @click="document.getElementById('service-restart-trigger')?.click()">
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
|
|
@ -117,25 +236,21 @@ class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-
|
|||
</svg>
|
||||
Restart
|
||||
</x-forms.button>
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
<x-forms.button isError title="Stop" @click="document.getElementById('service-stop-trigger')?.click()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
</svg>
|
||||
Stop
|
||||
</x-forms.button>
|
||||
@elseif (str($service->status)->contains('degraded'))
|
||||
<x-forms.button title="Restart" @click="$wire.dispatch('restartEvent')">
|
||||
<x-forms.button title="Restart" @click="document.getElementById('service-restart-trigger')?.click()">
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
|
|
@ -145,25 +260,21 @@ class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-
|
|||
</svg>
|
||||
Restart
|
||||
</x-forms.button>
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
<x-forms.button isError title="Stop" @click="document.getElementById('service-stop-trigger')?.click()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
</svg>
|
||||
Stop
|
||||
</x-forms.button>
|
||||
@elseif (str($service->status)->contains('exited'))
|
||||
<button @click="$wire.dispatch('startEvent')" class="gap-2 button">
|
||||
<button @click="document.getElementById('service-start-trigger')?.click()" class="gap-2 button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
|
|
@ -173,24 +284,20 @@ class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-
|
|||
Deploy
|
||||
</button>
|
||||
@else
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
</svg>
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
<button @click="$wire.dispatch('startEvent')" class="gap-2 button">
|
||||
<x-forms.button isError title="Stop" @click="document.getElementById('service-stop-trigger')?.click()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
<path
|
||||
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
|
||||
</path>
|
||||
</svg>
|
||||
Stop
|
||||
</x-forms.button>
|
||||
<button @click="document.getElementById('service-start-trigger')?.click()" class="gap-2 button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
|
|
@ -212,6 +319,51 @@ class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-
|
|||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@if ($service->isDeployable)
|
||||
<x-modal-confirmation title="Confirm Service Deployment?" buttonTitle="Deploy"
|
||||
submitAction="startEvent" :dispatchAction="true" :actions="['This service will be deployed.']"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<button id="service-start-trigger" type="button" class="hidden">Deploy</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<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" class="hidden">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" class="hidden">Stop</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Service Force Deployment?" buttonTitle="Force Deploy"
|
||||
submitAction="forceDeployEvent" :dispatchAction="true" :actions="['This service will be force deployed.']"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<button id="service-forceDeploy-trigger" type="button" class="hidden">Force Deploy</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Pull Latest Images & Restart?" buttonTitle="Pull Latest Images & Restart"
|
||||
submitAction="pullAndRestartEvent" :dispatchAction="true" :actions="['Latest images will be pulled and the service will be restarted.']"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<button id="service-pullAndRestart-trigger" type="button" class="hidden">Pull Latest Images & Restart</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
<x-modal-confirmation title="Confirm Force Cleanup Containers?" buttonTitle="Force Cleanup Containers"
|
||||
submitAction="cleanupEvent" :dispatchAction="true" :actions="['Service containers will be force cleaned up.']"
|
||||
:confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Confirm">
|
||||
<x-slot:trigger>
|
||||
<button id="service-cleanup-trigger" type="button" class="hidden">Force Cleanup Containers</button>
|
||||
</x-slot:trigger>
|
||||
</x-modal-confirmation>
|
||||
@endif
|
||||
|
||||
@script
|
||||
<script>
|
||||
$wire.$on('stopEvent', () => {
|
||||
|
|
@ -252,6 +404,9 @@ class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-
|
|||
window.dispatchEvent(new CustomEvent('startservice'));
|
||||
$wire.$call('pullAndRestartEvent');
|
||||
});
|
||||
$wire.$on('cleanupEvent', () => {
|
||||
$wire.$call('stop', true);
|
||||
});
|
||||
$wire.on('imagePulled', () => {
|
||||
window.dispatchEvent(new CustomEvent('startservice'));
|
||||
$wire.$dispatch('info', 'Restarting service.');
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<div>
|
||||
<livewire:project.service.heading :service="$service" :parameters="$parameters" :query="$query" />
|
||||
<div class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<div class="flex flex-col h-full gap-4 md:gap-8 md:flex-row">
|
||||
@if ($resourceType === 'database')
|
||||
<x-service-database.sidebar :parameters="$parameters" :serviceDatabase="$serviceDatabase" :isImportSupported="$isImportSupported" />
|
||||
@else
|
||||
<div class="sub-menu-wrapper">
|
||||
<div class="sub-menu-wrapper hidden md:flex">
|
||||
<a class="sub-menu-item"
|
||||
class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-active' : '' }}"
|
||||
{{ wireNavigate() }}
|
||||
|
|
|
|||
69
tests/Feature/MobileResourceMenuTest.php
Normal file
69
tests/Feature/MobileResourceMenuTest.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
it('uses native mobile menus for databases and services', function () {
|
||||
$applicationHeading = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php'));
|
||||
$databaseHeading = file_get_contents(resource_path('views/livewire/project/database/heading.blade.php'));
|
||||
$serviceHeading = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php'));
|
||||
|
||||
expect($applicationHeading)
|
||||
->toContain("'route' => 'project.application.command'")
|
||||
->toContain("'navigate' => false")
|
||||
->toContain("value.startsWith('location|')")
|
||||
->toContain('window.location.href = url');
|
||||
|
||||
expect($databaseHeading)
|
||||
->toContain('database-mobile-section')
|
||||
->toContain('<optgroup label="Database">')
|
||||
->toContain('<optgroup label="Configuration">')
|
||||
->toContain('<optgroup label="Actions">')
|
||||
->toContain("'route' => 'project.database.command'")
|
||||
->toContain("'navigate' => false")
|
||||
->toContain("value.startsWith('location|')")
|
||||
->toContain('window.location.href = url')
|
||||
->toContain('window.Livewire?.navigate ? window.Livewire.navigate(url) : window.location.href = url')
|
||||
->toContain("window.Livewire?.hook?.('morphed'")
|
||||
->toContain('x-model="selected"')
|
||||
->toContain('database-restart-trigger')
|
||||
->toContain('database-stop-trigger')
|
||||
->toContain('scrollbar hidden min-h-10')
|
||||
->not->toContain('<optgroup label="Links">')
|
||||
->not->toContain('@selected');
|
||||
|
||||
expect($serviceHeading)
|
||||
->toContain('service-mobile-section')
|
||||
->toContain('<optgroup label="Service">')
|
||||
->toContain('<optgroup label="Configuration">')
|
||||
->toContain('<optgroup label="Resource">')
|
||||
->toContain('<optgroup label="Links">')
|
||||
->toContain('<optgroup label="Actions">')
|
||||
->toContain("'route' => 'project.service.command'")
|
||||
->toContain("'navigate' => false")
|
||||
->toContain("value.startsWith('location|')")
|
||||
->toContain('window.location.href = url')
|
||||
->toContain('window.Livewire?.navigate ? window.Livewire.navigate(url) : window.location.href = url')
|
||||
->toContain("window.Livewire?.hook?.('morphed'")
|
||||
->toContain('x-model="selected"')
|
||||
->toContain('service-restart-trigger')
|
||||
->toContain('service-stop-trigger')
|
||||
->toContain('service-forceDeploy-trigger')
|
||||
->toContain('service-pullAndRestart-trigger')
|
||||
->toContain('scrollbar hidden min-h-10')
|
||||
->toContain('mb-4 w-full md:mb-0 md:hidden')
|
||||
->toContain('hidden flex-wrap items-center gap-2 md:flex')
|
||||
->not->toContain('order-first flex flex-wrap items-center gap-2 sm:order-last')
|
||||
->not->toContain('@selected');
|
||||
});
|
||||
|
||||
it('keeps configuration sidebars hidden until desktop breakpoint', function () {
|
||||
expect(file_get_contents(resource_path('views/livewire/project/database/configuration.blade.php')))
|
||||
->toContain('sub-menu-wrapper hidden md:flex');
|
||||
|
||||
expect(file_get_contents(resource_path('views/livewire/project/service/configuration.blade.php')))
|
||||
->toContain('sub-menu-wrapper hidden md:flex');
|
||||
|
||||
expect(file_get_contents(resource_path('views/livewire/project/service/index.blade.php')))
|
||||
->toContain('sub-menu-wrapper hidden md:flex');
|
||||
|
||||
expect(file_get_contents(resource_path('views/components/service-database/sidebar.blade.php')))
|
||||
->toContain('sub-menu-wrapper hidden md:flex');
|
||||
});
|
||||
|
|
@ -87,6 +87,35 @@
|
|||
|
||||
$response->assertSuccessful();
|
||||
$response->assertSee('Use a Build Server?');
|
||||
$response->assertSee('application-mobile-section');
|
||||
$response->assertSee('Application menu');
|
||||
$response->assertSee('<optgroup label="Application">', false);
|
||||
$response->assertSee('<optgroup label="Configuration">', false);
|
||||
$response->assertSee('<optgroup label="Links">', false);
|
||||
$response->assertSee('<optgroup label="Actions">', false);
|
||||
$response->assertSee('value="navigate|application|', false);
|
||||
$response->assertSee('value="navigate|configuration|', false);
|
||||
$response->assertSee('window.Livewire?.navigate ? window.Livewire.navigate(url) : window.location.href = url', false);
|
||||
$response->assertSee('value="action:force-deploy"', false);
|
||||
$response->assertSee('application-mobile-stop-trigger');
|
||||
$response->assertSee('application-mobile-deploy-trigger');
|
||||
$response->assertSee('application-mobile-restart-trigger');
|
||||
$response->assertSee('application-mobile-force-deploy-trigger');
|
||||
$response->assertSee('Confirm Application Deployment?');
|
||||
$response->assertSee('Confirm Application Restart?');
|
||||
$response->assertSee('Confirm Application Force Deployment?');
|
||||
$response->assertSee('sub-menu-wrapper hidden md:flex', false);
|
||||
$response->assertSee('scrollbar hidden min-h-10', false);
|
||||
$response->assertSee(route('project.application.deployment.index', [
|
||||
'project_uuid' => $this->project->uuid,
|
||||
'environment_uuid' => $this->environment->uuid,
|
||||
'application_uuid' => $this->application->uuid,
|
||||
]));
|
||||
$response->assertSee(route('project.application.environment-variables', [
|
||||
'project_uuid' => $this->project->uuid,
|
||||
'environment_uuid' => $this->environment->uuid,
|
||||
'application_uuid' => $this->application->uuid,
|
||||
]));
|
||||
$response->assertSee('form-control flex max-w-full flex-row items-center gap-4 py-1 pr-2', false);
|
||||
$response->assertSee('label flex w-full max-w-full min-w-0 items-center gap-4 px-0', false);
|
||||
$response->assertSee('flex min-w-0 grow gap-2 break-words', false);
|
||||
|
|
|
|||
|
|
@ -76,3 +76,39 @@ public function __construct(public string $status) {}
|
|||
->not->toContain('class="px-2"')
|
||||
->not->toContain('viewBox="0 0 256 256"');
|
||||
});
|
||||
|
||||
it('renders restart counts as warning badges', function () {
|
||||
$resource = new class
|
||||
{
|
||||
public string $status = 'running:unknown';
|
||||
|
||||
public int $restart_count = 9;
|
||||
|
||||
public ?int $max_restart_count = null;
|
||||
|
||||
public ?string $last_restart_type = null;
|
||||
|
||||
public $last_restart_at;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->last_restart_at = now();
|
||||
}
|
||||
};
|
||||
|
||||
$html = view('components.status.index', [
|
||||
'resource' => $resource,
|
||||
'showRefreshButton' => false,
|
||||
])->render();
|
||||
$statusIndex = file_get_contents(resource_path('views/components/status/index.blade.php'));
|
||||
|
||||
expect($html)
|
||||
->toContain('9x restarts')
|
||||
->toContain('border-yellow-300 bg-yellow-50')
|
||||
->not->toContain('(9x restarts)');
|
||||
|
||||
expect($statusIndex)
|
||||
->toContain('<x-status-badge')
|
||||
->toContain('restart_count')
|
||||
->not->toContain('class="text-xs dark:text-warning"');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue