Merge remote-tracking branch 'origin/next' into fix-ghe-app-install-url
This commit is contained in:
commit
96adf9b0f4
24 changed files with 1483 additions and 643 deletions
|
|
@ -3,34 +3,50 @@
|
||||||
Advanced
|
Advanced
|
||||||
</x-slot>
|
</x-slot>
|
||||||
@if ($application->status === 'running')
|
@if ($application->status === 'running')
|
||||||
<div class="dropdown-iteme" wire:click='force_deploy_without_cache'>
|
<x-modal-confirmation title="Confirm Application Force Deployment?" buttonTitle="Force deploy"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
|
submitAction="force_deploy_without_cache" :actions="[
|
||||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
'This application will be force deployed without build cache.',
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||||
<path
|
step2ButtonText="Confirm">
|
||||||
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" />
|
<x-slot:content>
|
||||||
<path
|
<div class="dropdown-iteme">
|
||||||
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" />
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
|
||||||
<path d="M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4" />
|
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M3 3l18 18" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
</svg>
|
<path
|
||||||
Force deploy (without
|
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" />
|
||||||
cache)
|
<path
|
||||||
</div>
|
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
|
@else
|
||||||
<div class="dropdown-item" wire:click='deploy(true)'>
|
<x-modal-confirmation title="Confirm Application Force Deployment?" buttonTitle="Force deploy"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" stroke-width="1.5"
|
submitAction="deploy(true)" :actions="[
|
||||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
'This application will be force deployed without build cache.',
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||||
<path
|
step2ButtonText="Confirm">
|
||||||
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" />
|
<x-slot:content>
|
||||||
<path
|
<div class="dropdown-item">
|
||||||
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" />
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" stroke-width="1.5"
|
||||||
<path d="M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4" />
|
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M3 3l18 18" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
</svg>
|
<path
|
||||||
Force deploy (without
|
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" />
|
||||||
cache)
|
<path
|
||||||
</div>
|
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
|
@endif
|
||||||
</x-dropdown>
|
</x-dropdown>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
'isImportSupported' => false,
|
'isImportSupported' => false,
|
||||||
])
|
])
|
||||||
|
|
||||||
<div class="sub-menu-wrapper">
|
<div class="sub-menu-wrapper hidden md:flex">
|
||||||
<a class="sub-menu-item"
|
<a class="sub-menu-item"
|
||||||
class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-active' : '' }}"
|
class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-active' : '' }}"
|
||||||
{{ wireNavigate() }}
|
{{ wireNavigate() }}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Advanced
|
Advanced
|
||||||
</x-slot>
|
</x-slot>
|
||||||
@if (str($service->status)->contains('running'))
|
@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"
|
<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">
|
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
Pull Latest Images & Restart
|
Pull Latest Images & Restart
|
||||||
</div>
|
</div>
|
||||||
@elseif (str($service->status)->contains('degraded'))
|
@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"
|
<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=""
|
stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke=""
|
||||||
style="--darkreader-inline-stroke: currentColor;" class="w-6 h-6" stroke-width="2">
|
style="--darkreader-inline-stroke: currentColor;" class="w-6 h-6" stroke-width="2">
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
Force Restart
|
Force Restart
|
||||||
</div>
|
</div>
|
||||||
@else
|
@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"
|
<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=""
|
stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke=""
|
||||||
style="--darkreader-inline-stroke: currentColor;" class="w-4 h-4" stroke-width="2">
|
style="--darkreader-inline-stroke: currentColor;" class="w-4 h-4" stroke-width="2">
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
</svg>
|
</svg>
|
||||||
Force Deploy
|
Force Deploy
|
||||||
</div>
|
</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">
|
<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" d="M26 20h-6v-2h6zm4 8h-6v-2h6zm-2-4h-6v-2h6z" />
|
||||||
<path fill="currentColor"
|
<path fill="currentColor"
|
||||||
|
|
|
||||||
38
resources/views/components/status-badge.blade.php
Normal file
38
resources/views/components/status-badge.blade.php
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
@props([
|
||||||
|
'label' => null,
|
||||||
|
'status' => null,
|
||||||
|
'type' => 'neutral',
|
||||||
|
'as' => 'span',
|
||||||
|
])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$typeClasses = [
|
||||||
|
'neutral' => 'border-neutral-200 bg-neutral-100 text-black dark:border-coolgray-300 dark:bg-coolgray-200 dark:text-white',
|
||||||
|
'success' => 'border-green-200 bg-green-50 text-green-800 dark:border-green-900 dark:bg-green-950/30 dark:text-green-300',
|
||||||
|
'warning' => 'border-yellow-300 bg-yellow-50 text-yellow-900 dark:border-yellow-800 dark:bg-yellow-950/30 dark:text-yellow-200',
|
||||||
|
'error' => 'border-red-300 bg-red-50 text-red-800 dark:border-red-900 dark:bg-red-950/30 dark:text-red-300',
|
||||||
|
];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@if ($as === 'button')
|
||||||
|
<button {{ $attributes->class([
|
||||||
|
'inline-flex h-5 max-w-full items-center gap-1 rounded-sm border px-1.5 text-xs font-medium leading-4 transition-colors',
|
||||||
|
$typeClasses[$type] ?? $typeClasses['neutral'],
|
||||||
|
])->merge(['type' => 'button']) }}>
|
||||||
|
{{ collect([$label, $status])->filter()->join(' ') }}
|
||||||
|
</button>
|
||||||
|
@elseif ($as === 'a')
|
||||||
|
<a {{ $attributes->class([
|
||||||
|
'inline-flex h-5 max-w-full items-center gap-1 rounded-sm border px-1.5 text-xs font-medium leading-4 transition-colors',
|
||||||
|
$typeClasses[$type] ?? $typeClasses['neutral'],
|
||||||
|
]) }}>
|
||||||
|
{{ collect([$label, $status])->filter()->join(' ') }}
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<span {{ $attributes->class([
|
||||||
|
'inline-flex h-5 max-w-full items-center gap-1 rounded-sm border px-1.5 text-xs font-medium leading-4',
|
||||||
|
$typeClasses[$type] ?? $typeClasses['neutral'],
|
||||||
|
]) }}>
|
||||||
|
{{ collect([$label, $status])->filter()->join(' ') }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
|
@ -2,32 +2,18 @@
|
||||||
'status' => 'Degraded',
|
'status' => 'Degraded',
|
||||||
])
|
])
|
||||||
@php
|
@php
|
||||||
// Handle both colon format (backend) and parentheses format (from services.blade.php)
|
|
||||||
// degraded:unhealthy → Degraded (unhealthy)
|
|
||||||
// degraded (unhealthy) → degraded (unhealthy) (already formatted, display as-is)
|
|
||||||
|
|
||||||
if (str($status)->contains('(')) {
|
if (str($status)->contains('(')) {
|
||||||
// Already in parentheses format from services.blade.php - use as-is
|
|
||||||
$displayStatus = $status;
|
$displayStatus = $status;
|
||||||
$healthStatus = str($status)->after('(')->before(')')->trim()->value();
|
$healthStatus = null;
|
||||||
} elseif (str($status)->contains(':') && !str($status)->startsWith('Proxy')) {
|
} elseif (str($status)->contains(':') && ! str($status)->startsWith('Proxy')) {
|
||||||
// Colon format from backend - transform it
|
|
||||||
$parts = explode(':', $status);
|
$parts = explode(':', $status);
|
||||||
$displayStatus = str($parts[0])->headline();
|
$displayStatus = str($parts[0])->headline()->value();
|
||||||
$healthStatus = $parts[1] ?? null;
|
$healthStatus = $parts[1] ?? null;
|
||||||
} else {
|
} else {
|
||||||
// Simple status without health
|
$displayStatus = str($status)->headline()->value();
|
||||||
$displayStatus = str($status)->headline();
|
|
||||||
$healthStatus = null;
|
$healthStatus = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$badgeStatus = $healthStatus ? "{$displayStatus} ({$healthStatus})" : $displayStatus;
|
||||||
@endphp
|
@endphp
|
||||||
<div class="flex items-center" >
|
<x-status-badge status="{{ $badgeStatus }}" type="warning" />
|
||||||
<x-loading wire:loading.delay.longer />
|
|
||||||
<span wire:loading.remove.delay.longer class="flex items-center">
|
|
||||||
<div class="badge badge-warning"></div>
|
|
||||||
<div class="pl-2 pr-1 text-xs font-bold dark:text-warning">{{ $displayStatus }}</div>
|
|
||||||
@if ($healthStatus && !str($displayStatus)->contains('('))
|
|
||||||
<div class="text-xs dark:text-warning">({{ $healthStatus }})</div>
|
|
||||||
@endif
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
|
||||||
|
|
@ -18,33 +18,18 @@
|
||||||
<x-status.stopped :status="$resource->status" />
|
<x-status.stopped :status="$resource->status" />
|
||||||
@endif
|
@endif
|
||||||
@if (isset($resource->restart_count) && $resource->restart_count > 0 && (!str($resource->status)->startsWith('exited') || $stoppedAfterRestartLimit))
|
@if (isset($resource->restart_count) && $resource->restart_count > 0 && (!str($resource->status)->startsWith('exited') || $stoppedAfterRestartLimit))
|
||||||
<div class="flex items-center">
|
<x-status-badge status="{{ $resource->restart_count }}x restarts" type="warning"
|
||||||
<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() }}">
|
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>
|
|
||||||
@endif
|
@endif
|
||||||
@if ($stoppedAfterRestartLimit)
|
@if ($stoppedAfterRestartLimit)
|
||||||
<div class="flex items-center">
|
<x-status-badge status="Stopped after reaching restart limit ({{ $resource->restart_count }}/{{ $resource->max_restart_count }})."
|
||||||
<span class="text-xs dark:text-warning" title="Container has crashed and Coolify stopped it after {{ $resource->restart_count }} restart attempts.">
|
type="warning"
|
||||||
Stopped after reaching restart limit ({{ $resource->restart_count }}/{{ $resource->max_restart_count }}).
|
title="Container has crashed and Coolify stopped it after {{ $resource->restart_count }} restart attempts." />
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
@endif
|
@endif
|
||||||
@if (!str($resource->status)->contains('exited') && $showRefreshButton)
|
@if (!str($resource->status)->contains('exited') && $showRefreshButton)
|
||||||
<button wire:loading.remove.delay.shortest wire:target="manualCheckStatus" title="Refresh Status" wire:click='manualCheckStatus'
|
<x-status-badge as="button" wire:target="manualCheckStatus" wire:loading.attr="disabled"
|
||||||
class="dark:hover:fill-white fill-black dark:fill-warning">
|
wire:click='manualCheckStatus' status="Refresh" type="neutral" title="Refresh Status"
|
||||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
aria-label="Refresh status"
|
||||||
<path
|
class="min-w-[4.5rem] justify-center cursor-pointer border-transparent hover:bg-neutral-200 disabled:cursor-wait disabled:opacity-70 dark:hover:bg-coolgray-300" />
|
||||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button wire:loading.delay.shortest wire:target="manualCheckStatus" title="Refreshing Status" wire:click='manualCheckStatus'
|
|
||||||
class="dark:hover:fill-white fill-black dark:fill-warning">
|
|
||||||
<svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path
|
|
||||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,42 +5,23 @@
|
||||||
'noLoading' => false,
|
'noLoading' => false,
|
||||||
])
|
])
|
||||||
@php
|
@php
|
||||||
// Handle both colon format (backend) and parentheses format (from services.blade.php)
|
|
||||||
// starting:unknown → Starting (unknown)
|
|
||||||
// starting (unknown) → starting (unknown) (already formatted, display as-is)
|
|
||||||
|
|
||||||
if (str($status)->contains('(')) {
|
if (str($status)->contains('(')) {
|
||||||
// Already in parentheses format from services.blade.php - use as-is
|
|
||||||
$displayStatus = $status;
|
$displayStatus = $status;
|
||||||
$healthStatus = str($status)->after('(')->before(')')->trim()->value();
|
$healthStatus = null;
|
||||||
} elseif (str($status)->contains(':') && !str($status)->startsWith('Proxy')) {
|
} elseif (str($status)->contains(':') && ! str($status)->startsWith('Proxy')) {
|
||||||
// Colon format from backend - transform it
|
|
||||||
$parts = explode(':', $status);
|
$parts = explode(':', $status);
|
||||||
$displayStatus = str($parts[0])->headline();
|
$displayStatus = str($parts[0])->headline()->value();
|
||||||
$healthStatus = $parts[1] ?? null;
|
$healthStatus = $parts[1] ?? null;
|
||||||
} else {
|
} else {
|
||||||
// Simple status without health
|
$displayStatus = str($status)->headline()->value();
|
||||||
$displayStatus = str($status)->headline();
|
|
||||||
$healthStatus = null;
|
$healthStatus = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$badgeStatus = $healthStatus ? "{$displayStatus} ({$healthStatus})" : $displayStatus;
|
||||||
@endphp
|
@endphp
|
||||||
<div class="flex items-center">
|
@if ($lastDeploymentLink)
|
||||||
@if (!$noLoading)
|
<x-status-badge as="a" href="{{ $lastDeploymentLink }}" target="_blank" status="{{ $badgeStatus }}" type="warning"
|
||||||
<x-loading wire:loading.delay.longer />
|
title="{{ $title }}" class="cursor-pointer underline" />
|
||||||
@endif
|
@else
|
||||||
<span wire:loading.remove.delay.longer class="flex items-center">
|
<x-status-badge status="{{ $badgeStatus }}" type="warning" title="{{ $title }}" />
|
||||||
<div class="badge badge-warning"></div>
|
@endif
|
||||||
<div class="pl-2 pr-1 text-xs font-bold dark:text-warning" @if($title) title="{{$title}}" @endif>
|
|
||||||
@if ($lastDeploymentLink)
|
|
||||||
<a href="{{ $lastDeploymentLink }}" target="_blank" class="underline cursor-pointer">
|
|
||||||
{{ $displayStatus }}
|
|
||||||
</a>
|
|
||||||
@else
|
|
||||||
{{ $displayStatus }}
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@if ($healthStatus && !str($displayStatus)->contains('('))
|
|
||||||
<div class="text-xs dark:text-warning">({{ $healthStatus }})</div>
|
|
||||||
@endif
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
|
||||||
|
|
@ -5,80 +5,47 @@
|
||||||
'noLoading' => false,
|
'noLoading' => false,
|
||||||
])
|
])
|
||||||
@php
|
@php
|
||||||
// Handle both colon format (backend) and parentheses format (from services.blade.php)
|
|
||||||
// running:healthy → Running (healthy)
|
|
||||||
// running (healthy) → running (healthy) (already formatted, display as-is)
|
|
||||||
|
|
||||||
if (str($status)->contains('(')) {
|
if (str($status)->contains('(')) {
|
||||||
// Already in parentheses format from services.blade.php - use as-is
|
|
||||||
$displayStatus = $status;
|
$displayStatus = $status;
|
||||||
$healthStatus = str($status)->after('(')->before(')')->trim()->value();
|
$healthStatus = null;
|
||||||
} elseif (str($status)->contains(':') && !str($status)->startsWith('Proxy')) {
|
} elseif (str($status)->contains(':') && ! str($status)->startsWith('Proxy')) {
|
||||||
// Colon format from backend - transform it
|
|
||||||
$parts = explode(':', $status);
|
$parts = explode(':', $status);
|
||||||
$displayStatus = str($parts[0])->headline();
|
$displayStatus = str($parts[0])->headline()->value();
|
||||||
$healthStatus = $parts[1] ?? null;
|
$healthStatus = $parts[1] ?? null;
|
||||||
} else {
|
} else {
|
||||||
// Simple status without health
|
$displayStatus = str($status)->headline()->value();
|
||||||
$displayStatus = str($status)->headline();
|
|
||||||
$healthStatus = null;
|
$healthStatus = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$badgeStatus = $healthStatus ? "{$displayStatus} ({$healthStatus})" : $displayStatus;
|
||||||
|
$showUnknownHelper = ! str($status)->startsWith('Proxy') && (str($status)->contains('unknown') || str($healthStatus)->contains('unknown'));
|
||||||
|
$showUnhealthyHelper = ! str($status)->startsWith('Proxy') && (str($status)->contains('unhealthy') || str($healthStatus)->contains('unhealthy'));
|
||||||
@endphp
|
@endphp
|
||||||
<div class="flex items-center">
|
<div class="flex items-center gap-1">
|
||||||
<div class="flex items-center">
|
@if ($lastDeploymentLink)
|
||||||
<div wire:loading.delay.longer wire:target="checkProxy(true)" class="badge badge-warning"></div>
|
<x-status-badge as="a" href="{{ $lastDeploymentLink }}" target="_blank" status="{{ $badgeStatus }}" type="success"
|
||||||
<div wire:loading.remove.delay.longer wire:target="checkProxy(true)" class="badge badge-success"></div>
|
title="{{ $title }}" class="cursor-pointer underline" />
|
||||||
<div class="pl-2 pr-1 text-xs font-bold text-success"
|
@else
|
||||||
@if ($title) title="{{ $title }}" @endif>
|
<x-status-badge status="{{ $badgeStatus }}" type="success" title="{{ $title }}" />
|
||||||
@if ($lastDeploymentLink)
|
@endif
|
||||||
<a href="{{ $lastDeploymentLink }}" target="_blank" class="underline cursor-pointer">
|
@if ($showUnknownHelper)
|
||||||
{{ $displayStatus }}
|
<div>
|
||||||
</a>
|
|
||||||
@else
|
|
||||||
{{ $displayStatus }}
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@if ($healthStatus && !str($displayStatus)->contains('('))
|
|
||||||
<div class="text-xs text-success">({{ $healthStatus }})</div>
|
|
||||||
@endif
|
|
||||||
@php
|
|
||||||
$showUnknownHelper =
|
|
||||||
!str($status)->startsWith('Proxy') &&
|
|
||||||
(str($status)->contains('unknown') || str($healthStatus)->contains('unknown'));
|
|
||||||
$showUnhealthyHelper =
|
|
||||||
!str($status)->startsWith('Proxy') &&
|
|
||||||
(str($status)->contains('unhealthy') || str($healthStatus)->contains('unhealthy'));
|
|
||||||
@endphp
|
|
||||||
@if ($showUnknownHelper)
|
|
||||||
<div class="px-2">
|
|
||||||
<x-helper
|
<x-helper
|
||||||
helper="No health check configured. <span class='dark:text-warning text-coollabs'>The resource may be functioning normally.</span><br><br>Traefik and Caddy will route traffic to this container even without a health check. However, configuring a health check is recommended to ensure the resource is ready before receiving traffic.<br><br>More details in the <a href='https://coolify.io/docs/knowledge-base/proxy/traefik/healthchecks' class='underline dark:text-warning text-coollabs' target='_blank'>documentation</a>.">
|
helper="No health check configured. <span class='dark:text-warning text-coollabs'>The resource may be functioning normally.</span><br><br>Traefik and Caddy will route traffic to this container even without a health check. However, configuring a health check is recommended to ensure the resource is ready before receiving traffic.<br><br>More details in the <a href='https://coolify.io/docs/knowledge-base/proxy/traefik/healthchecks' class='underline dark:text-warning text-coollabs' target='_blank'>documentation</a>.">
|
||||||
<x-slot:icon>
|
<x-slot:icon>
|
||||||
<svg class="hidden w-4 h-4 dark:text-warning lg:block" viewBox="0 0 256 256"
|
<x-status-badge status="No health check" type="warning" class="cursor-help" />
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill="currentColor"
|
|
||||||
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16">
|
|
||||||
</path>
|
|
||||||
</svg>
|
|
||||||
</x-slot:icon>
|
</x-slot:icon>
|
||||||
</x-helper>
|
</x-helper>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@if ($showUnhealthyHelper)
|
@if ($showUnhealthyHelper)
|
||||||
<div class="px-2">
|
<div>
|
||||||
<x-helper
|
<x-helper
|
||||||
helper="Unhealthy state. <span class='dark:text-warning text-coollabs'>The health check is failing.</span><br><br>This resource will <span class='dark:text-warning text-coollabs'>NOT work with Traefik</span> as it expects a healthy state. Your action is required to fix the health check or the underlying issue causing it to fail.<br><br>More details in the <a href='https://coolify.io/docs/knowledge-base/proxy/traefik/healthchecks' class='underline dark:text-warning text-coollabs' target='_blank'>documentation</a>.">
|
helper="Unhealthy state. <span class='dark:text-warning text-coollabs'>The health check is failing.</span><br><br>This resource will <span class='dark:text-warning text-coollabs'>NOT work with Traefik</span> as it expects a healthy state. Your action is required to fix the health check or the underlying issue causing it to fail.<br><br>More details in the <a href='https://coolify.io/docs/knowledge-base/proxy/traefik/healthchecks' class='underline dark:text-warning text-coollabs' target='_blank'>documentation</a>.">
|
||||||
<x-slot:icon>
|
<x-slot:icon>
|
||||||
<svg class="hidden w-4 h-4 dark:text-warning lg:block" viewBox="0 0 256 256"
|
<x-status-badge status="Unhealthy" type="warning" class="cursor-help" />
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill="currentColor"
|
|
||||||
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16">
|
|
||||||
</path>
|
|
||||||
</svg>
|
|
||||||
</x-slot:icon>
|
</x-slot:icon>
|
||||||
</x-helper>
|
</x-helper>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,19 +14,9 @@
|
||||||
<x-status.stopped :status="$displayStatus" />
|
<x-status.stopped :status="$displayStatus" />
|
||||||
@endif
|
@endif
|
||||||
@if (!str($complexStatus)->contains('exited') && $showRefreshButton)
|
@if (!str($complexStatus)->contains('exited') && $showRefreshButton)
|
||||||
<button wire:loading.remove.delay.shortest wire:target="manualCheckStatus" title="Refresh Status" wire:click='manualCheckStatus'
|
<x-status-badge as="button" wire:target="manualCheckStatus" wire:loading.attr="disabled"
|
||||||
class="dark:hover:fill-white fill-black dark:fill-warning">
|
wire:click='manualCheckStatus' status="Refresh" type="neutral" title="Refresh Status"
|
||||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
aria-label="Refresh status"
|
||||||
<path
|
class="min-w-[4.5rem] justify-center cursor-pointer border-transparent hover:bg-neutral-200 disabled:cursor-wait disabled:opacity-70 dark:hover:bg-coolgray-300" />
|
||||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button wire:loading.delay.shortest wire:target="manualCheckStatus" title="Refreshing Status" wire:click='manualCheckStatus'
|
|
||||||
class="dark:hover:fill-white fill-black dark:fill-warning">
|
|
||||||
<svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path
|
|
||||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,46 +3,12 @@
|
||||||
'noLoading' => false,
|
'noLoading' => false,
|
||||||
])
|
])
|
||||||
@php
|
@php
|
||||||
// Handle both colon format (backend) and parentheses format (from services.blade.php)
|
|
||||||
// For exited containers, health status is hidden (health checks don't run on stopped containers)
|
|
||||||
// exited:unhealthy → Exited
|
|
||||||
// exited (unhealthy) → Exited
|
|
||||||
|
|
||||||
if (str($status)->contains('(')) {
|
if (str($status)->contains('(')) {
|
||||||
// Already in parentheses format from services.blade.php - use as-is
|
$displayStatus = str($status)->before('(')->trim()->headline()->value();
|
||||||
$displayStatus = $status;
|
|
||||||
$healthStatus = str($status)->after('(')->before(')')->trim()->value();
|
|
||||||
|
|
||||||
// Don't show health status for exited containers (health checks don't run on stopped containers)
|
|
||||||
if (str($displayStatus)->lower()->contains('exited')) {
|
|
||||||
$displayStatus = str($status)->before('(')->trim()->headline();
|
|
||||||
$healthStatus = null;
|
|
||||||
}
|
|
||||||
} elseif (str($status)->contains(':')) {
|
} elseif (str($status)->contains(':')) {
|
||||||
// Colon format from backend - transform it
|
$displayStatus = str(explode(':', $status)[0])->headline()->value();
|
||||||
$parts = explode(':', $status);
|
|
||||||
$displayStatus = str($parts[0])->headline();
|
|
||||||
$healthStatus = $parts[1] ?? null;
|
|
||||||
|
|
||||||
// Don't show health status for exited containers (health checks don't run on stopped containers)
|
|
||||||
if (str($displayStatus)->lower()->contains('exited')) {
|
|
||||||
$healthStatus = null;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Simple status without health
|
$displayStatus = str($status)->headline()->value();
|
||||||
$displayStatus = str($status)->headline();
|
|
||||||
$healthStatus = null;
|
|
||||||
}
|
}
|
||||||
@endphp
|
@endphp
|
||||||
<div class="flex items-center">
|
<x-status-badge status="{{ $displayStatus }}" type="error" />
|
||||||
@if (!$noLoading)
|
|
||||||
<x-loading wire:loading.delay.longer />
|
|
||||||
@endif
|
|
||||||
<span wire:loading.remove.delay.longer class="flex items-center">
|
|
||||||
<div class="badge badge-error "></div>
|
|
||||||
<div class="pl-2 pr-1 text-xs font-bold text-error">{{ $displayStatus }}</div>
|
|
||||||
@if ($healthStatus && !str($displayStatus)->contains('('))
|
|
||||||
<div class="text-xs text-error">({{ $healthStatus }})</div>
|
|
||||||
@endif
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
|
||||||
|
|
@ -6,55 +6,128 @@
|
||||||
<livewire:project.shared.configuration-checker :resource="$application" />
|
<livewire:project.shared.configuration-checker :resource="$application" />
|
||||||
<livewire:project.application.heading :application="$application" />
|
<livewire:project.application.heading :application="$application" />
|
||||||
|
|
||||||
<div class="flex flex-col h-full gap-8 sm:flex-row">
|
@php
|
||||||
<div class="sub-menu-wrapper">
|
$applicationRouteParameters = [
|
||||||
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
'project_uuid' => $project->uuid,
|
||||||
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>
|
'environment_uuid' => $environment->uuid,
|
||||||
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
'application_uuid' => $application->uuid,
|
||||||
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"
|
$configurationMenuItems = [
|
||||||
href="{{ route('project.application.swarm', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}"><span class="menu-item-label">Swarm</span>
|
[
|
||||||
|
'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>
|
</a>
|
||||||
@endif
|
@endforeach
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full sm:flex-grow">
|
<div class="w-full md:flex-grow">
|
||||||
@if ($currentRoute === 'project.application.configuration')
|
@if ($currentRoute === 'project.application.configuration')
|
||||||
<livewire:project.application.general :application="$application" />
|
<livewire:project.application.general :application="$application" />
|
||||||
@elseif ($currentRoute === 'project.application.swarm' && $application->destination->server->isSwarm())
|
@elseif ($currentRoute === 'project.application.swarm' && $application->destination->server->isSwarm())
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,373 @@
|
||||||
<nav wire:poll.10000ms="checkStatus" class="pb-6">
|
<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" />
|
<x-resources.breadcrumbs :resource="$application" :parameters="$parameters" :title="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" />
|
||||||
<div class="navbar-main">
|
<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
|
<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.application.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
<a class="hidden md:block shrink-0 {{ request()->routeIs('project.application.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||||
href="{{ route('project.application.configuration', $parameters) }}">
|
href="{{ route('project.application.configuration', $parameters) }}">
|
||||||
Configuration
|
Configuration
|
||||||
</a>
|
</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) }}">
|
href="{{ route('project.application.deployment.index', $parameters) }}">
|
||||||
Deployments
|
Deployments
|
||||||
</a>
|
</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) }}">
|
href="{{ route('project.application.logs', $parameters) }}">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
Logs
|
Logs
|
||||||
|
|
@ -24,13 +380,13 @@ class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-
|
||||||
</a>
|
</a>
|
||||||
@if (!$application->destination->server->isSwarm())
|
@if (!$application->destination->server->isSwarm())
|
||||||
@can('canAccessTerminal')
|
@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) }}">
|
href="{{ route('project.application.command', $parameters) }}">
|
||||||
Terminal
|
Terminal
|
||||||
</a>
|
</a>
|
||||||
@endcan
|
@endcan
|
||||||
@endif
|
@endif
|
||||||
<div class="shrink-0">
|
<div class="hidden shrink-0 md:block">
|
||||||
<x-applications.links :application="$application" />
|
<x-applications.links :application="$application" />
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</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))
|
@if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
|
||||||
<div>Please load a Compose file.</div>
|
<div>Please load a Compose file.</div>
|
||||||
@else
|
@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">
|
<div class="hidden flex-wrap items-center gap-2 md:flex">
|
||||||
@if (!$application->destination->server->isSwarm())
|
@if (!$application->destination->server->isSwarm())
|
||||||
<div>
|
<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">
|
<div class="flex flex-wrap gap-2">
|
||||||
@if (!str($application->status)->startsWith('exited'))
|
@if (!str($application->status)->startsWith('exited'))
|
||||||
@if (!$application->destination->server->isSwarm())
|
@if (!$application->destination->server->isSwarm())
|
||||||
<x-forms.button title="With rolling update if possible" wire:click='deploy'>
|
<x-modal-confirmation title="Confirm Application Deployment?" buttonTitle="Redeploy"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-orange-400"
|
submitAction="deploy" :actions="[
|
||||||
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
'This application will be redeployed.',
|
||||||
stroke-linecap="round" stroke-linejoin="round">
|
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
step2ButtonText="Confirm">
|
||||||
<path
|
<x-slot:content>
|
||||||
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">
|
<x-forms.button title="With rolling update if possible">
|
||||||
</path>
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-orange-400"
|
||||||
<path d="M7.05 11.038v-3.988"></path>
|
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
||||||
</svg>
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
Redeploy
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
</x-forms.button>
|
<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
|
@endif
|
||||||
@if ($application->build_pack !== 'dockercompose')
|
@if ($application->build_pack !== 'dockercompose')
|
||||||
@if ($application->destination->server->isSwarm())
|
@if ($application->destination->server->isSwarm())
|
||||||
<x-forms.button title="Redeploy Swarm Service (rolling update)" wire:click='deploy'>
|
<x-modal-confirmation title="Confirm Application Deployment?" buttonTitle="Update Service"
|
||||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
submitAction="deploy" :actions="[
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
'This Swarm service will be updated with a rolling deployment.',
|
||||||
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||||
stroke-linejoin="round" stroke-width="2">
|
step2ButtonText="Confirm">
|
||||||
<path
|
<x-slot:content>
|
||||||
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
<x-forms.button title="Redeploy Swarm Service (rolling update)">
|
||||||
<path d="M20 4v5h-5" />
|
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||||
</g>
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
</svg>
|
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||||
Update Service
|
stroke-linejoin="round" stroke-width="2">
|
||||||
</x-forms.button>
|
<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
|
@else
|
||||||
<x-forms.button title="Restart without rebuilding" wire:click='restart'>
|
<x-modal-confirmation title="Confirm Application Restart?" buttonTitle="Restart"
|
||||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
submitAction="restart" :actions="[
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
'This application will be restarted without rebuilding.',
|
||||||
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||||
stroke-linejoin="round" stroke-width="2">
|
step2ButtonText="Confirm">
|
||||||
<path
|
<x-slot:content>
|
||||||
d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
<x-forms.button title="Restart without rebuilding">
|
||||||
<path d="M20 4v5h-5" />
|
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||||
</g>
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
</svg>
|
<g fill="none" stroke="currentColor" stroke-linecap="round"
|
||||||
Restart
|
stroke-linejoin="round" stroke-width="2">
|
||||||
</x-forms.button>
|
<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
|
||||||
@endif
|
@endif
|
||||||
<x-modal-confirmation title="Confirm Application Stopping?" buttonTitle="Stop"
|
<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-slot:button-title>
|
||||||
</x-modal-confirmation>
|
</x-modal-confirmation>
|
||||||
@else
|
@else
|
||||||
<x-forms.button wire:click='deploy'>
|
<x-modal-confirmation title="Confirm Application Deployment?" buttonTitle="Deploy"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning"
|
submitAction="deploy" :actions="[
|
||||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"
|
'This application will be deployed.',
|
||||||
stroke-linecap="round" stroke-linejoin="round">
|
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
step2ButtonText="Confirm">
|
||||||
<path d="M7 4v16l13 -8z" />
|
<x-slot:content>
|
||||||
</svg>
|
<x-forms.button>
|
||||||
Deploy
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning"
|
||||||
</x-forms.button>
|
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
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
<h1>Configuration</h1>
|
<h1>Configuration</h1>
|
||||||
<livewire:project.shared.configuration-checker :resource="$database" />
|
<livewire:project.shared.configuration-checker :resource="$database" />
|
||||||
<livewire:project.database.heading :database="$database" />
|
<livewire:project.database.heading :database="$database" />
|
||||||
<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">
|
||||||
<div class="sub-menu-wrapper">
|
<div class="sub-menu-wrapper hidden md:flex">
|
||||||
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
<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>
|
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"
|
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,74 @@
|
||||||
<nav wire:poll.10000ms="checkStatus" class="pb-6">
|
<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-resources.breadcrumbs :resource="$database" :parameters="$parameters" />
|
||||||
<x-slide-over @startdatabase.window="slideOverOpen = true" closeWithX fullScreen>
|
<x-slide-over @startdatabase.window="slideOverOpen = true" closeWithX fullScreen>
|
||||||
<x-slot:title>Database Startup</x-slot:title>
|
<x-slot:title>Database Startup</x-slot:title>
|
||||||
|
|
@ -9,8 +79,76 @@
|
||||||
</x-slot:content>
|
</x-slot:content>
|
||||||
</x-slide-over>
|
</x-slide-over>
|
||||||
<div class="navbar-main">
|
<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
|
<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() }}
|
<a class="shrink-0 {{ request()->routeIs('project.database.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||||
href="{{ route('project.database.configuration', $parameters) }}">
|
href="{{ route('project.database.configuration', $parameters) }}">
|
||||||
Configuration
|
Configuration
|
||||||
|
|
@ -37,89 +175,37 @@ class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-
|
||||||
</a>
|
</a>
|
||||||
@endif
|
@endif
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@if ($database->destination->server->isFunctional())
|
@if ($database->destination->server->isFunctional())
|
||||||
<div class="flex flex-wrap gap-2 items-center">
|
<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">
|
<div class="hidden flex-wrap items-center gap-2 md:flex">
|
||||||
@if (!str($database->status)->startsWith('exited'))
|
@if (!str($database->status)->startsWith('exited'))
|
||||||
<x-modal-confirmation title="Confirm Database Restart?" buttonTitle="Restart" submitAction="restart"
|
<x-forms.button title="Restart" @click="document.getElementById('database-restart-trigger')?.click()">
|
||||||
:actions="[
|
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||||
'This database will be unavailable during the restart.',
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
'If the database is currently in use data could be lost.',
|
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||||
]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Restart Database"
|
stroke-width="2">
|
||||||
:dispatchEvent="true" dispatchEventType="restartEvent">
|
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
||||||
<x-slot:button-title>
|
<path d="M20 4v5h-5" />
|
||||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
</g>
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
</svg>
|
||||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
Restart
|
||||||
stroke-width="2">
|
</x-forms.button>
|
||||||
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
|
<x-forms.button isError title="Stop" @click="document.getElementById('database-stop-trigger')?.click()">
|
||||||
<path d="M20 4v5h-5" />
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||||
</g>
|
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||||
</svg>
|
stroke-linejoin="round">
|
||||||
Restart
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
</x-slot:button-title>
|
<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">
|
||||||
</x-modal-confirmation>
|
</path>
|
||||||
<x-modal-confirmation title="Confirm Database Stopping?" buttonTitle="Stop" submitAction="stop"
|
<path
|
||||||
:checkboxes="$checkboxes" :actions="[
|
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">
|
||||||
'This database will be stopped.',
|
</path>
|
||||||
'If the database is currently in use data could be lost.',
|
</svg>
|
||||||
'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).',
|
Stop
|
||||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
</x-forms.button>
|
||||||
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>
|
|
||||||
@else
|
@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"
|
<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-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||||
stroke-linejoin="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>
|
<div class="text-error">Underlying server is not functional.</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</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>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
</x-slot>
|
</x-slot>
|
||||||
<livewire:project.service.heading :service="$service" :parameters="$parameters" :query="$query" />
|
<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">
|
||||||
<div class="sub-menu-wrapper">
|
<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>
|
<a class="sub-menu-item" target="_blank" href="{{ $service->documentation() }}"><span class="menu-item-label">Documentation</span>
|
||||||
<x-external-link /></a>
|
<x-external-link /></a>
|
||||||
<a class='sub-menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
|
<a class='sub-menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<div>
|
<div>
|
||||||
<livewire:project.service.heading :service="$service" :parameters="$parameters" :query="$query" />
|
<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" />
|
<x-service-database.sidebar :parameters="$parameters" :serviceDatabase="$serviceDatabase" :isImportSupported="$isImportSupported" />
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<x-slot:title>
|
<x-slot:title>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,94 @@
|
||||||
<div wire:poll.10000ms="checkStatus" class="pb-6">
|
<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" />
|
<livewire:project.shared.configuration-checker :resource="$service" />
|
||||||
<x-slide-over @startservice.window="slideOverOpen = true" closeWithX fullScreen>
|
<x-slide-over @startservice.window="slideOverOpen = true" closeWithX fullScreen>
|
||||||
<x-slot:title>Service Startup</x-slot:title>
|
<x-slot:title>Service Startup</x-slot:title>
|
||||||
|
|
@ -9,8 +99,109 @@
|
||||||
<h1>{{ $title }}</h1>
|
<h1>{{ $title }}</h1>
|
||||||
<x-resources.breadcrumbs :resource="$service" :parameters="$parameters" />
|
<x-resources.breadcrumbs :resource="$service" :parameters="$parameters" />
|
||||||
<div class="navbar-main" x-data">
|
<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
|
<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() }}
|
<a class="shrink-0 {{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||||
href="{{ route('project.service.configuration', $parameters) }}">
|
href="{{ route('project.service.configuration', $parameters) }}">
|
||||||
<button>Configuration</button>
|
<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" />
|
<x-services.links :service="$service" />
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@if ($service->isDeployable)
|
@if ($service->isDeployable)
|
||||||
<div class="order-first flex flex-wrap items-center gap-2 sm:order-last">
|
<div class="hidden flex-wrap items-center gap-2 md:flex">
|
||||||
<div class="md:hidden">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<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">
|
|
||||||
<x-services.advanced :service="$service" />
|
<x-services.advanced :service="$service" />
|
||||||
@if (str($service->status)->contains('running'))
|
@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">
|
<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"
|
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||||
stroke-width="2">
|
stroke-width="2">
|
||||||
|
|
@ -117,25 +236,21 @@ class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-
|
||||||
</svg>
|
</svg>
|
||||||
Restart
|
Restart
|
||||||
</x-forms.button>
|
</x-forms.button>
|
||||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
<x-forms.button isError title="Stop" @click="document.getElementById('service-stop-trigger')?.click()">
|
||||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||||
<x-slot:button-title>
|
stroke-linejoin="round">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
<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">
|
||||||
stroke-linejoin="round">
|
</path>
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<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">
|
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>
|
</path>
|
||||||
<path
|
</svg>
|
||||||
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">
|
Stop
|
||||||
</path>
|
</x-forms.button>
|
||||||
</svg>
|
|
||||||
Stop
|
|
||||||
</x-slot:button-title>
|
|
||||||
</x-modal-confirmation>
|
|
||||||
@elseif (str($service->status)->contains('degraded'))
|
@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">
|
<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"
|
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||||
stroke-width="2">
|
stroke-width="2">
|
||||||
|
|
@ -145,25 +260,21 @@ class="scrollbar flex min-h-10 w-full flex-nowrap items-center gap-6 overflow-x-
|
||||||
</svg>
|
</svg>
|
||||||
Restart
|
Restart
|
||||||
</x-forms.button>
|
</x-forms.button>
|
||||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
<x-forms.button isError title="Stop" @click="document.getElementById('service-stop-trigger')?.click()">
|
||||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||||
<x-slot:button-title>
|
stroke-linejoin="round">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
<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">
|
||||||
stroke-linejoin="round">
|
</path>
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<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">
|
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>
|
</path>
|
||||||
<path
|
</svg>
|
||||||
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">
|
Stop
|
||||||
</path>
|
</x-forms.button>
|
||||||
</svg>
|
|
||||||
Stop
|
|
||||||
</x-slot:button-title>
|
|
||||||
</x-modal-confirmation>
|
|
||||||
@elseif (str($service->status)->contains('exited'))
|
@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"
|
<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-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||||
stroke-linejoin="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
|
Deploy
|
||||||
</button>
|
</button>
|
||||||
@else
|
@else
|
||||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" :dispatchEvent="true"
|
<x-forms.button isError title="Stop" @click="document.getElementById('service-stop-trigger')?.click()">
|
||||||
submitAction="stop" dispatchEventType="stopEvent" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]"
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
||||||
:confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" step2ButtonText="Confirm">
|
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||||
<x-slot:button-title>
|
stroke-linejoin="round">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
<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">
|
||||||
stroke-linejoin="round">
|
</path>
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<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">
|
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>
|
</path>
|
||||||
<path
|
</svg>
|
||||||
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">
|
Stop
|
||||||
</path>
|
</x-forms.button>
|
||||||
</svg>
|
<button @click="document.getElementById('service-start-trigger')?.click()" class="gap-2 button">
|
||||||
Stop
|
|
||||||
</x-slot:button-title>
|
|
||||||
</x-modal-confirmation>
|
|
||||||
<button @click="$wire.dispatch('startEvent')" 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"
|
<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-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||||
stroke-linejoin="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>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</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
|
||||||
<script>
|
<script>
|
||||||
$wire.$on('stopEvent', () => {
|
$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'));
|
window.dispatchEvent(new CustomEvent('startservice'));
|
||||||
$wire.$call('pullAndRestartEvent');
|
$wire.$call('pullAndRestartEvent');
|
||||||
});
|
});
|
||||||
|
$wire.$on('cleanupEvent', () => {
|
||||||
|
$wire.$call('stop', true);
|
||||||
|
});
|
||||||
$wire.on('imagePulled', () => {
|
$wire.on('imagePulled', () => {
|
||||||
window.dispatchEvent(new CustomEvent('startservice'));
|
window.dispatchEvent(new CustomEvent('startservice'));
|
||||||
$wire.$dispatch('info', 'Restarting service.');
|
$wire.$dispatch('info', 'Restarting service.');
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<div>
|
<div>
|
||||||
<livewire:project.service.heading :service="$service" :parameters="$parameters" :query="$query" />
|
<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')
|
@if ($resourceType === 'database')
|
||||||
<x-service-database.sidebar :parameters="$parameters" :serviceDatabase="$serviceDatabase" :isImportSupported="$isImportSupported" />
|
<x-service-database.sidebar :parameters="$parameters" :serviceDatabase="$serviceDatabase" :isImportSupported="$isImportSupported" />
|
||||||
@else
|
@else
|
||||||
<div class="sub-menu-wrapper">
|
<div class="sub-menu-wrapper hidden md:flex">
|
||||||
<a class="sub-menu-item"
|
<a class="sub-menu-item"
|
||||||
class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-active' : '' }}"
|
class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-active' : '' }}"
|
||||||
{{ wireNavigate() }}
|
{{ wireNavigate() }}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,15 @@ class="w-4 h-4 dark:text-warning text-coollabs"
|
||||||
@endcan
|
@endcan
|
||||||
</span>
|
</span>
|
||||||
@endif
|
@endif
|
||||||
<div @class(['pt-2' => $isApplication, 'text-xs'])>{{ formatContainerStatus($resource->status) }}</div>
|
<div @class(['pt-2' => $isApplication])>
|
||||||
|
@if (str($resource->status)->contains('running'))
|
||||||
|
<x-status-badge status="{{ formatContainerStatus($resource->status) }}" type="success" />
|
||||||
|
@elseif (str($resource->status)->contains(['starting', 'restarting', 'degraded']))
|
||||||
|
<x-status-badge status="{{ formatContainerStatus($resource->status) }}" type="warning" />
|
||||||
|
@else
|
||||||
|
<x-status-badge status="{{ formatContainerStatus($resource->status) }}" type="error" />
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center px-4">
|
<div class="flex items-center px-4">
|
||||||
@if ($isDatabase && ($resource->isBackupSolutionAvailable() || $resource->is_migrated))
|
@if ($isDatabase && ($resource->isBackupSolutionAvailable() || $resource->is_migrated))
|
||||||
|
|
|
||||||
|
|
@ -12,65 +12,50 @@
|
||||||
<livewire:activity-monitor header="Logs" fullHeight />
|
<livewire:activity-monitor header="Logs" fullHeight />
|
||||||
</x-slot:content>
|
</x-slot:content>
|
||||||
</x-slide-over>
|
</x-slide-over>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex flex-col gap-1.5">
|
||||||
<h1>Server</h1>
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
@if ($server->proxySet())
|
<h1>Server</h1>
|
||||||
<div class="flex">
|
@if ($server->proxySet() || $server->isSentinelEnabled())
|
||||||
<div class="flex items-center">
|
<div data-testid="server-status-summary" class="flex flex-wrap items-center gap-2">
|
||||||
@if ($proxyStatus === 'running')
|
@if ($server->proxySet())
|
||||||
<x-status.running status="Proxy Running" noLoading />
|
<div class="flex items-center gap-1">
|
||||||
@elseif ($proxyStatus === 'restarting')
|
@if ($proxyStatus === 'running')
|
||||||
<x-status.restarting status="Proxy Restarting" noLoading />
|
<x-status-badge label="Proxy" status="Running" type="success" />
|
||||||
@elseif ($proxyStatus === 'stopping')
|
@elseif ($proxyStatus === 'restarting')
|
||||||
<x-status.restarting status="Proxy Stopping" noLoading />
|
<x-status-badge label="Proxy" status="Restarting" type="warning" />
|
||||||
@elseif ($proxyStatus === 'starting')
|
@elseif ($proxyStatus === 'stopping')
|
||||||
<x-status.restarting status="Proxy Starting" noLoading />
|
<x-status-badge label="Proxy" status="Stopping" type="warning" />
|
||||||
@elseif (data_get($server, 'proxy.force_stop'))
|
@elseif ($proxyStatus === 'starting')
|
||||||
<div wire:loading.remove wire:target="checkProxy">
|
<x-status-badge label="Proxy" status="Starting" type="warning" />
|
||||||
<x-status.stopped status="Proxy Stopped (Force Stop)" noLoading />
|
@elseif (data_get($server, 'proxy.force_stop'))
|
||||||
</div>
|
<x-status-badge wire:loading.remove wire:target="checkProxy" label="Proxy"
|
||||||
@elseif ($proxyStatus === 'exited')
|
status="Force stopped" type="error" />
|
||||||
<div wire:loading.remove wire:target="checkProxy">
|
@elseif ($proxyStatus === 'exited')
|
||||||
<x-status.stopped status="Proxy Exited" noLoading />
|
<x-status-badge wire:loading.remove wire:target="checkProxy" label="Proxy" status="Exited"
|
||||||
|
type="error" />
|
||||||
|
@endif
|
||||||
|
<x-status-badge wire:loading wire:target="checkProxy" label="Proxy" status="Checking..."
|
||||||
|
type="warning" />
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<div wire:loading wire:target="checkProxy" class="badge badge-warning"></div>
|
@if ($server->isSentinelEnabled())
|
||||||
<div wire:loading wire:target="checkProxy"
|
@if ($server->isSentinelLive())
|
||||||
class="pl-2 pr-1 text-xs font-bold tracking-wider dark:text-warning">
|
<x-status-badge label="Sentinel" status="In sync" type="success" />
|
||||||
Checking Ports Availability...
|
@else
|
||||||
</div>
|
<x-status-badge label="Sentinel" status="Out of sync" type="error" />
|
||||||
@if ($proxyStatus !== 'exited')
|
@endif
|
||||||
<button wire:loading.remove title="Refresh Status" wire:click='checkProxyStatus'
|
@endif
|
||||||
class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
|
@if ($server->proxySet())
|
||||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
<x-status-badge as="button" wire:target="checkProxyStatus" wire:loading.attr="disabled"
|
||||||
<path
|
wire:click='checkProxyStatus' status="Refresh" type="neutral" title="Refresh Status"
|
||||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
aria-label="Refresh proxy status"
|
||||||
</svg>
|
class="min-w-[4.5rem] justify-center cursor-pointer border-transparent hover:bg-neutral-200 disabled:cursor-wait disabled:opacity-70 dark:hover:bg-coolgray-300" />
|
||||||
</button>
|
|
||||||
<button wire:loading title="Refreshing Status" wire:click='checkProxyStatus'
|
|
||||||
class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
|
|
||||||
<svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path
|
|
||||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
@endif
|
||||||
@endif
|
</div>
|
||||||
@if ($server->isSentinelEnabled())
|
<div class="subtitle">{{ data_get($server, 'name') }}</div>
|
||||||
<div class="flex">
|
|
||||||
<div class="flex items-center">
|
|
||||||
@if ($server->isSentinelLive())
|
|
||||||
<x-status.running status="Sentinel In Sync" noLoading />
|
|
||||||
@else
|
|
||||||
<x-status.stopped status="Sentinel Out of Sync" noLoading />
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
<div class="subtitle">{{ data_get($server, 'name') }}</div>
|
|
||||||
<div class="navbar-main">
|
<div class="navbar-main">
|
||||||
<nav
|
<nav
|
||||||
class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar min-h-10 whitespace-nowrap pt-2">
|
class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar min-h-10 whitespace-nowrap pt-2">
|
||||||
|
|
|
||||||
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->assertSuccessful();
|
||||||
$response->assertSee('Use a Build Server?');
|
$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('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('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);
|
$response->assertSee('flex min-w-0 grow gap-2 break-words', false);
|
||||||
|
|
|
||||||
27
tests/Feature/ServerNavbarStatusLayoutTest.php
Normal file
27
tests/Feature/ServerNavbarStatusLayoutTest.php
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
it('uses left aligned single color status badges in the server header', function () {
|
||||||
|
$navbarView = file_get_contents(resource_path('views/livewire/server/navbar.blade.php'));
|
||||||
|
$badgeView = file_get_contents(resource_path('views/components/status-badge.blade.php'));
|
||||||
|
|
||||||
|
expect($navbarView)
|
||||||
|
->toContain('data-testid="server-status-summary"')
|
||||||
|
->toContain('<x-status-badge')
|
||||||
|
->toContain('label="Proxy"')
|
||||||
|
->toContain('label="Sentinel"')
|
||||||
|
->toContain('as="button"')
|
||||||
|
->toContain("wire:click='checkProxyStatus'")
|
||||||
|
->toContain('status="Refresh"')
|
||||||
|
->toContain('border-transparent')
|
||||||
|
->toContain('wire:loading.attr="disabled"')
|
||||||
|
->not->toContain('status="Refreshing..."')
|
||||||
|
->not->toContain('justify-between')
|
||||||
|
->not->toContain('<x-status.stopped status="Proxy Exited" noLoading />')
|
||||||
|
->not->toContain('<x-status.running status="Sentinel In Sync" noLoading />');
|
||||||
|
|
||||||
|
expect($badgeView)
|
||||||
|
->not->toContain('text-neutral-500')
|
||||||
|
->not->toContain('dark:text-neutral-400')
|
||||||
|
->toContain('<button')
|
||||||
|
->toContain("merge(['type' => 'button'])");
|
||||||
|
});
|
||||||
114
tests/Feature/StatusBadgeComponentsTest.php
Normal file
114
tests/Feature/StatusBadgeComponentsTest.php
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
it('renders application and database statuses as shared status badges', function (string $status, string $expectedText) {
|
||||||
|
$resource = new class($status)
|
||||||
|
{
|
||||||
|
public function __construct(public string $status) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
$html = view('components.status.index', [
|
||||||
|
'resource' => $resource,
|
||||||
|
'showRefreshButton' => false,
|
||||||
|
])->render();
|
||||||
|
|
||||||
|
expect($html)
|
||||||
|
->toContain('inline-flex h-5 max-w-full items-center gap-1 rounded-sm border')
|
||||||
|
->toContain($expectedText)
|
||||||
|
->not->toContain('badge-success')
|
||||||
|
->not->toContain('badge-warning')
|
||||||
|
->not->toContain('badge-error');
|
||||||
|
})->with([
|
||||||
|
'running healthy' => ['running:healthy', 'Running (healthy)'],
|
||||||
|
'starting unknown' => ['starting:unknown', 'Starting (unknown)'],
|
||||||
|
'degraded unhealthy' => ['degraded:unhealthy', 'Degraded (unhealthy)'],
|
||||||
|
'exited unhealthy' => ['exited:unhealthy', 'Exited'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
it('renders service container statuses as shared status badges', function () {
|
||||||
|
$html = view('components.status.services', [
|
||||||
|
'complexStatus' => 'running:healthy',
|
||||||
|
'showRefreshButton' => false,
|
||||||
|
])->render();
|
||||||
|
|
||||||
|
expect($html)
|
||||||
|
->toContain('inline-flex h-5 max-w-full items-center gap-1 rounded-sm border')
|
||||||
|
->toContain('Running (healthy)')
|
||||||
|
->not->toContain('badge-success');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses a shared refresh badge for resource status refresh actions', function () {
|
||||||
|
$statusIndex = file_get_contents(resource_path('views/components/status/index.blade.php'));
|
||||||
|
$serviceStatus = file_get_contents(resource_path('views/components/status/services.blade.php'));
|
||||||
|
|
||||||
|
expect($statusIndex)
|
||||||
|
->toContain('<x-status-badge as="button"')
|
||||||
|
->toContain("wire:click='manualCheckStatus'")
|
||||||
|
->toContain('wire:loading.attr="disabled"')
|
||||||
|
->not->toContain('status="Refreshing..."')
|
||||||
|
->toContain('wire:loading.attr="disabled"')
|
||||||
|
->not->toContain('status="Refreshing..."')
|
||||||
|
->not->toContain('<svg');
|
||||||
|
|
||||||
|
expect(file_get_contents(resource_path('views/livewire/project/service/resource-card.blade.php')))
|
||||||
|
->toContain('<x-status-badge')
|
||||||
|
->toContain('formatContainerStatus($resource->status)');
|
||||||
|
|
||||||
|
expect($serviceStatus)
|
||||||
|
->toContain('<x-status-badge as="button"')
|
||||||
|
->toContain("wire:click='manualCheckStatus'")
|
||||||
|
->not->toContain('<svg');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders health warning helpers as badges instead of warning icons', function () {
|
||||||
|
$html = view('components.status.running', [
|
||||||
|
'status' => 'running:unknown',
|
||||||
|
])->render();
|
||||||
|
$runningStatus = file_get_contents(resource_path('views/components/status/running.blade.php'));
|
||||||
|
|
||||||
|
expect($html)
|
||||||
|
->toContain('No health check')
|
||||||
|
->toContain('inline-flex h-5 max-w-full items-center gap-1 rounded-sm border')
|
||||||
|
->not->toContain('<svg');
|
||||||
|
|
||||||
|
expect($runningStatus)
|
||||||
|
->toContain('<x-status-badge')
|
||||||
|
->toContain('class="flex items-center gap-1"')
|
||||||
|
->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