feat(status): unify resource status badges
Introduce a shared status badge component and reuse it across resource status views, service cards, and the server navbar. Replace icon-only refresh controls with consistent badge buttons and cover the new status layout with feature tests.
This commit is contained in:
parent
7053f560c5
commit
51583069f4
11 changed files with 245 additions and 229 deletions
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',
|
||||
])
|
||||
@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('(')) {
|
||||
// Already in parentheses format from services.blade.php - use as-is
|
||||
$displayStatus = $status;
|
||||
$healthStatus = str($status)->after('(')->before(')')->trim()->value();
|
||||
} elseif (str($status)->contains(':') && !str($status)->startsWith('Proxy')) {
|
||||
// Colon format from backend - transform it
|
||||
$healthStatus = null;
|
||||
} elseif (str($status)->contains(':') && ! str($status)->startsWith('Proxy')) {
|
||||
$parts = explode(':', $status);
|
||||
$displayStatus = str($parts[0])->headline();
|
||||
$displayStatus = str($parts[0])->headline()->value();
|
||||
$healthStatus = $parts[1] ?? null;
|
||||
} else {
|
||||
// Simple status without health
|
||||
$displayStatus = str($status)->headline();
|
||||
$displayStatus = str($status)->headline()->value();
|
||||
$healthStatus = null;
|
||||
}
|
||||
|
||||
$badgeStatus = $healthStatus ? "{$displayStatus} ({$healthStatus})" : $displayStatus;
|
||||
@endphp
|
||||
<div class="flex items-center" >
|
||||
<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>
|
||||
<x-status-badge status="{{ $badgeStatus }}" type="warning" />
|
||||
|
|
|
|||
|
|
@ -32,19 +32,9 @@
|
|||
</div>
|
||||
@endif
|
||||
@if (!str($resource->status)->contains('exited') && $showRefreshButton)
|
||||
<button wire:loading.remove.delay.shortest wire:target="manualCheckStatus" title="Refresh Status" wire:click='manualCheckStatus'
|
||||
class="dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4" 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>
|
||||
<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>
|
||||
<x-status-badge as="button" wire:target="manualCheckStatus" wire:loading.attr="disabled"
|
||||
wire:click='manualCheckStatus' status="Refresh" type="neutral" title="Refresh Status"
|
||||
aria-label="Refresh status"
|
||||
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" />
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,42 +5,23 @@
|
|||
'noLoading' => false,
|
||||
])
|
||||
@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('(')) {
|
||||
// Already in parentheses format from services.blade.php - use as-is
|
||||
$displayStatus = $status;
|
||||
$healthStatus = str($status)->after('(')->before(')')->trim()->value();
|
||||
} elseif (str($status)->contains(':') && !str($status)->startsWith('Proxy')) {
|
||||
// Colon format from backend - transform it
|
||||
$healthStatus = null;
|
||||
} elseif (str($status)->contains(':') && ! str($status)->startsWith('Proxy')) {
|
||||
$parts = explode(':', $status);
|
||||
$displayStatus = str($parts[0])->headline();
|
||||
$displayStatus = str($parts[0])->headline()->value();
|
||||
$healthStatus = $parts[1] ?? null;
|
||||
} else {
|
||||
// Simple status without health
|
||||
$displayStatus = str($status)->headline();
|
||||
$displayStatus = str($status)->headline()->value();
|
||||
$healthStatus = null;
|
||||
}
|
||||
|
||||
$badgeStatus = $healthStatus ? "{$displayStatus} ({$healthStatus})" : $displayStatus;
|
||||
@endphp
|
||||
<div class="flex items-center">
|
||||
@if (!$noLoading)
|
||||
<x-loading wire:loading.delay.longer />
|
||||
@endif
|
||||
<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" @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>
|
||||
@if ($lastDeploymentLink)
|
||||
<x-status-badge as="a" href="{{ $lastDeploymentLink }}" target="_blank" status="{{ $badgeStatus }}" type="warning"
|
||||
title="{{ $title }}" class="cursor-pointer underline" />
|
||||
@else
|
||||
<x-status-badge status="{{ $badgeStatus }}" type="warning" title="{{ $title }}" />
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -5,80 +5,47 @@
|
|||
'noLoading' => false,
|
||||
])
|
||||
@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('(')) {
|
||||
// Already in parentheses format from services.blade.php - use as-is
|
||||
$displayStatus = $status;
|
||||
$healthStatus = str($status)->after('(')->before(')')->trim()->value();
|
||||
} elseif (str($status)->contains(':') && !str($status)->startsWith('Proxy')) {
|
||||
// Colon format from backend - transform it
|
||||
$healthStatus = null;
|
||||
} elseif (str($status)->contains(':') && ! str($status)->startsWith('Proxy')) {
|
||||
$parts = explode(':', $status);
|
||||
$displayStatus = str($parts[0])->headline();
|
||||
$displayStatus = str($parts[0])->headline()->value();
|
||||
$healthStatus = $parts[1] ?? null;
|
||||
} else {
|
||||
// Simple status without health
|
||||
$displayStatus = str($status)->headline();
|
||||
$displayStatus = str($status)->headline()->value();
|
||||
$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
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center">
|
||||
<div wire:loading.delay.longer wire:target="checkProxy(true)" class="badge badge-warning"></div>
|
||||
<div wire:loading.remove.delay.longer wire:target="checkProxy(true)" class="badge badge-success"></div>
|
||||
<div class="pl-2 pr-1 text-xs font-bold text-success"
|
||||
@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 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">
|
||||
<div class="flex items-center gap-1">
|
||||
@if ($lastDeploymentLink)
|
||||
<x-status-badge as="a" href="{{ $lastDeploymentLink }}" target="_blank" status="{{ $badgeStatus }}" type="success"
|
||||
title="{{ $title }}" class="cursor-pointer underline" />
|
||||
@else
|
||||
<x-status-badge status="{{ $badgeStatus }}" type="success" title="{{ $title }}" />
|
||||
@endif
|
||||
@if ($showUnknownHelper)
|
||||
<div>
|
||||
<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>.">
|
||||
<x-slot:icon>
|
||||
<svg class="hidden w-4 h-4 dark:text-warning lg:block" viewBox="0 0 256 256"
|
||||
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-status-badge status="No health check" type="warning" class="cursor-help" />
|
||||
</x-slot:icon>
|
||||
</x-helper>
|
||||
</div>
|
||||
@endif
|
||||
@if ($showUnhealthyHelper)
|
||||
<div class="px-2">
|
||||
@endif
|
||||
@if ($showUnhealthyHelper)
|
||||
<div>
|
||||
<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>.">
|
||||
<x-slot:icon>
|
||||
<svg class="hidden w-4 h-4 dark:text-warning lg:block" viewBox="0 0 256 256"
|
||||
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-status-badge status="Unhealthy" type="warning" class="cursor-help" />
|
||||
</x-slot:icon>
|
||||
</x-helper>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,19 +14,9 @@
|
|||
<x-status.stopped :status="$displayStatus" />
|
||||
@endif
|
||||
@if (!str($complexStatus)->contains('exited') && $showRefreshButton)
|
||||
<button wire:loading.remove.delay.shortest wire:target="manualCheckStatus" title="Refresh Status" wire:click='manualCheckStatus'
|
||||
class="dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4" 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>
|
||||
<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>
|
||||
<x-status-badge as="button" wire:target="manualCheckStatus" wire:loading.attr="disabled"
|
||||
wire:click='manualCheckStatus' status="Refresh" type="neutral" title="Refresh Status"
|
||||
aria-label="Refresh status"
|
||||
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" />
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,46 +3,12 @@
|
|||
'noLoading' => false,
|
||||
])
|
||||
@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('(')) {
|
||||
// Already in parentheses format from services.blade.php - use as-is
|
||||
$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;
|
||||
}
|
||||
$displayStatus = str($status)->before('(')->trim()->headline()->value();
|
||||
} elseif (str($status)->contains(':')) {
|
||||
// Colon format from backend - transform it
|
||||
$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;
|
||||
}
|
||||
$displayStatus = str(explode(':', $status)[0])->headline()->value();
|
||||
} else {
|
||||
// Simple status without health
|
||||
$displayStatus = str($status)->headline();
|
||||
$healthStatus = null;
|
||||
$displayStatus = str($status)->headline()->value();
|
||||
}
|
||||
@endphp
|
||||
<div class="flex items-center">
|
||||
@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>
|
||||
<x-status-badge status="{{ $displayStatus }}" type="error" />
|
||||
|
|
|
|||
|
|
@ -43,7 +43,15 @@ class="w-4 h-4 dark:text-warning text-coollabs"
|
|||
@endcan
|
||||
</span>
|
||||
@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 class="flex items-center px-4">
|
||||
@if ($isDatabase && ($resource->isBackupSolutionAvailable() || $resource->is_migrated))
|
||||
|
|
|
|||
|
|
@ -12,65 +12,50 @@
|
|||
<livewire:activity-monitor header="Logs" fullHeight />
|
||||
</x-slot:content>
|
||||
</x-slide-over>
|
||||
<div class="flex items-center gap-2">
|
||||
<h1>Server</h1>
|
||||
@if ($server->proxySet())
|
||||
<div class="flex">
|
||||
<div class="flex items-center">
|
||||
@if ($proxyStatus === 'running')
|
||||
<x-status.running status="Proxy Running" noLoading />
|
||||
@elseif ($proxyStatus === 'restarting')
|
||||
<x-status.restarting status="Proxy Restarting" noLoading />
|
||||
@elseif ($proxyStatus === 'stopping')
|
||||
<x-status.restarting status="Proxy Stopping" noLoading />
|
||||
@elseif ($proxyStatus === 'starting')
|
||||
<x-status.restarting status="Proxy Starting" noLoading />
|
||||
@elseif (data_get($server, 'proxy.force_stop'))
|
||||
<div wire:loading.remove wire:target="checkProxy">
|
||||
<x-status.stopped status="Proxy Stopped (Force Stop)" noLoading />
|
||||
</div>
|
||||
@elseif ($proxyStatus === 'exited')
|
||||
<div wire:loading.remove wire:target="checkProxy">
|
||||
<x-status.stopped status="Proxy Exited" noLoading />
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h1>Server</h1>
|
||||
@if ($server->proxySet() || $server->isSentinelEnabled())
|
||||
<div data-testid="server-status-summary" class="flex flex-wrap items-center gap-2">
|
||||
@if ($server->proxySet())
|
||||
<div class="flex items-center gap-1">
|
||||
@if ($proxyStatus === 'running')
|
||||
<x-status-badge label="Proxy" status="Running" type="success" />
|
||||
@elseif ($proxyStatus === 'restarting')
|
||||
<x-status-badge label="Proxy" status="Restarting" type="warning" />
|
||||
@elseif ($proxyStatus === 'stopping')
|
||||
<x-status-badge label="Proxy" status="Stopping" type="warning" />
|
||||
@elseif ($proxyStatus === 'starting')
|
||||
<x-status-badge label="Proxy" status="Starting" type="warning" />
|
||||
@elseif (data_get($server, 'proxy.force_stop'))
|
||||
<x-status-badge wire:loading.remove wire:target="checkProxy" label="Proxy"
|
||||
status="Force stopped" type="error" />
|
||||
@elseif ($proxyStatus === 'exited')
|
||||
<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>
|
||||
@endif
|
||||
<div wire:loading wire:target="checkProxy" class="badge badge-warning"></div>
|
||||
<div wire:loading wire:target="checkProxy"
|
||||
class="pl-2 pr-1 text-xs font-bold tracking-wider dark:text-warning">
|
||||
Checking Ports Availability...
|
||||
</div>
|
||||
@if ($proxyStatus !== 'exited')
|
||||
<button wire:loading.remove title="Refresh Status" wire:click='checkProxyStatus'
|
||||
class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4" 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>
|
||||
<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>
|
||||
@if ($server->isSentinelEnabled())
|
||||
@if ($server->isSentinelLive())
|
||||
<x-status-badge label="Sentinel" status="In sync" type="success" />
|
||||
@else
|
||||
<x-status-badge label="Sentinel" status="Out of sync" type="error" />
|
||||
@endif
|
||||
@endif
|
||||
@if ($server->proxySet())
|
||||
<x-status-badge as="button" wire:target="checkProxyStatus" wire:loading.attr="disabled"
|
||||
wire:click='checkProxyStatus' status="Refresh" type="neutral" title="Refresh Status"
|
||||
aria-label="Refresh proxy status"
|
||||
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" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@if ($server->isSentinelEnabled())
|
||||
<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
|
||||
@endif
|
||||
</div>
|
||||
<div class="subtitle">{{ data_get($server, 'name') }}</div>
|
||||
</div>
|
||||
<div class="subtitle">{{ data_get($server, 'name') }}</div>
|
||||
<div class="navbar-main">
|
||||
<nav
|
||||
class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar min-h-10 whitespace-nowrap pt-2">
|
||||
|
|
|
|||
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'])");
|
||||
});
|
||||
78
tests/Feature/StatusBadgeComponentsTest.php
Normal file
78
tests/Feature/StatusBadgeComponentsTest.php
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<?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"');
|
||||
});
|
||||
Loading…
Reference in a new issue