diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 384b960ef..8a278476e 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -3153,3 +3153,46 @@ function generateDockerComposeServiceName(mixed $services, int $pullRequestId = return $collection; } + +/** + * Transform colon-delimited status format to human-readable parentheses format. + * + * Handles Docker container status formats with optional health check status and exclusion modifiers. + * + * Examples: + * - running:healthy → Running (healthy) + * - running:unhealthy:excluded → Running (unhealthy, excluded) + * - exited:excluded → Exited (excluded) + * - Proxy:running → Proxy:running (preserved as-is for headline formatting) + * - running → Running + * + * @param string $status The status string to format + * @return string The formatted status string + */ +function formatContainerStatus(string $status): string +{ + // Preserve Proxy statuses as-is (they follow different format) + if (str($status)->startsWith('Proxy')) { + return str($status)->headline()->value(); + } + + // Check for :excluded suffix + $isExcluded = str($status)->endsWith(':excluded'); + $parts = explode(':', $status); + + if ($isExcluded) { + if (count($parts) === 3) { + // Has health status: running:unhealthy:excluded → Running (unhealthy, excluded) + return str($parts[0])->headline().' ('.$parts[1].', excluded)'; + } else { + // No health status: exited:excluded → Exited (excluded) + return str($parts[0])->headline().' (excluded)'; + } + } elseif (count($parts) >= 2) { + // Regular colon format: running:healthy → Running (healthy) + return str($parts[0])->headline().' ('.$parts[1].')'; + } else { + // Simple status: running → Running + return str($status)->headline()->value(); + } +} diff --git a/resources/views/components/status/services.blade.php b/resources/views/components/status/services.blade.php index 1897781ba..9c7a870c7 100644 --- a/resources/views/components/status/services.blade.php +++ b/resources/views/components/status/services.blade.php @@ -1,26 +1,5 @@ @php - // Transform colon format to human-readable format for UI display - // running:healthy → Running (healthy) - // running:unhealthy:excluded → Running (unhealthy, excluded) - // exited:excluded → Exited (excluded) - $isExcluded = str($complexStatus)->endsWith(':excluded'); - $parts = explode(':', $complexStatus); - - if ($isExcluded) { - if (count($parts) === 3) { - // Has health status: running:unhealthy:excluded → Running (unhealthy, excluded) - $displayStatus = str($parts[0])->headline() . ' (' . $parts[1] . ', excluded)'; - } else { - // No health status: exited:excluded → Exited (excluded) - $displayStatus = str($parts[0])->headline() . ' (excluded)'; - } - } elseif (count($parts) >= 2 && !str($complexStatus)->startsWith('Proxy')) { - // Regular colon format: running:healthy → Running (healthy) - $displayStatus = str($parts[0])->headline() . ' (' . $parts[1] . ')'; - } else { - // No transformation needed (simple status or already in parentheses format) - $displayStatus = str($complexStatus)->headline(); - } + $displayStatus = formatContainerStatus($complexStatus); @endphp @if (str($displayStatus)->lower()->contains('running')) diff --git a/resources/views/livewire/project/service/configuration.blade.php b/resources/views/livewire/project/service/configuration.blade.php index cfe79e22d..9b81e4bec 100644 --- a/resources/views/livewire/project/service/configuration.blade.php +++ b/resources/views/livewire/project/service/configuration.blade.php @@ -90,31 +90,7 @@ class="w-4 h-4 dark:text-warning text-coollabs" @endcan @endif - @php - // Transform colon format to human-readable format - // running:healthy → Running (healthy) - // running:unhealthy:excluded → Running (unhealthy, excluded) - $appStatus = $application->status; - $isExcluded = str($appStatus)->endsWith(':excluded'); - $parts = explode(':', $appStatus); - - if ($isExcluded) { - if (count($parts) === 3) { - // Has health status: running:unhealthy:excluded → Running (unhealthy, excluded) - $appStatus = str($parts[0])->headline() . ' (' . $parts[1] . ', excluded)'; - } else { - // No health status: exited:excluded → Exited (excluded) - $appStatus = str($parts[0])->headline() . ' (excluded)'; - } - } elseif (count($parts) >= 2 && !str($appStatus)->startsWith('Proxy')) { - // Regular colon format: running:healthy → Running (healthy) - $appStatus = str($parts[0])->headline() . ' (' . $parts[1] . ')'; - } else { - // Simple status or already in parentheses format - $appStatus = str($appStatus)->headline(); - } - @endphp -
{{ $appStatus }}
+
{{ formatContainerStatus($application->status) }}
description) {{ Str::limit($database->description, 60) }} @endif - @php - // Transform colon format to human-readable format - // running:healthy → Running (healthy) - // running:unhealthy:excluded → Running (unhealthy, excluded) - $dbStatus = $database->status; - $isExcluded = str($dbStatus)->endsWith(':excluded'); - $parts = explode(':', $dbStatus); - - if ($isExcluded) { - if (count($parts) === 3) { - // Has health status: running:unhealthy:excluded → Running (unhealthy, excluded) - $dbStatus = str($parts[0])->headline() . ' (' . $parts[1] . ', excluded)'; - } else { - // No health status: exited:excluded → Exited (excluded) - $dbStatus = str($parts[0])->headline() . ' (excluded)'; - } - } elseif (count($parts) >= 2 && !str($dbStatus)->startsWith('Proxy')) { - // Regular colon format: running:healthy → Running (healthy) - $dbStatus = str($parts[0])->headline() . ' (' . $parts[1] . ')'; - } else { - // Simple status or already in parentheses format - $dbStatus = str($dbStatus)->headline(); - } - @endphp -
{{ $dbStatus }}
+
{{ formatContainerStatus($database->status) }}
@if ($database->isBackupSolutionAvailable() || $database->is_migrated) diff --git a/tests/Unit/FormatContainerStatusTest.php b/tests/Unit/FormatContainerStatusTest.php new file mode 100644 index 000000000..f24aa8c52 --- /dev/null +++ b/tests/Unit/FormatContainerStatusTest.php @@ -0,0 +1,201 @@ +toBe('Running (healthy)'); + }); + + it('transforms running:unhealthy to Running (unhealthy)', function () { + $result = formatContainerStatus('running:unhealthy'); + + expect($result)->toBe('Running (unhealthy)'); + }); + + it('transforms exited:0 to Exited (0)', function () { + $result = formatContainerStatus('exited:0'); + + expect($result)->toBe('Exited (0)'); + }); + + it('transforms restarting:starting to Restarting (starting)', function () { + $result = formatContainerStatus('restarting:starting'); + + expect($result)->toBe('Restarting (starting)'); + }); + }); + + describe('excluded suffix handling', function () { + it('transforms running:unhealthy:excluded to Running (unhealthy, excluded)', function () { + $result = formatContainerStatus('running:unhealthy:excluded'); + + expect($result)->toBe('Running (unhealthy, excluded)'); + }); + + it('transforms running:healthy:excluded to Running (healthy, excluded)', function () { + $result = formatContainerStatus('running:healthy:excluded'); + + expect($result)->toBe('Running (healthy, excluded)'); + }); + + it('transforms exited:excluded to Exited (excluded)', function () { + $result = formatContainerStatus('exited:excluded'); + + expect($result)->toBe('Exited (excluded)'); + }); + + it('transforms stopped:excluded to Stopped (excluded)', function () { + $result = formatContainerStatus('stopped:excluded'); + + expect($result)->toBe('Stopped (excluded)'); + }); + }); + + describe('simple status format', function () { + it('transforms running to Running', function () { + $result = formatContainerStatus('running'); + + expect($result)->toBe('Running'); + }); + + it('transforms exited to Exited', function () { + $result = formatContainerStatus('exited'); + + expect($result)->toBe('Exited'); + }); + + it('transforms stopped to Stopped', function () { + $result = formatContainerStatus('stopped'); + + expect($result)->toBe('Stopped'); + }); + + it('transforms restarting to Restarting', function () { + $result = formatContainerStatus('restarting'); + + expect($result)->toBe('Restarting'); + }); + + it('transforms degraded to Degraded', function () { + $result = formatContainerStatus('degraded'); + + expect($result)->toBe('Degraded'); + }); + }); + + describe('Proxy status preservation', function () { + it('preserves Proxy:running without parsing colons', function () { + $result = formatContainerStatus('Proxy:running'); + + expect($result)->toBe('Proxy:running'); + }); + + it('preserves Proxy:exited without parsing colons', function () { + $result = formatContainerStatus('Proxy:exited'); + + expect($result)->toBe('Proxy:exited'); + }); + + it('preserves Proxy:healthy without parsing colons', function () { + $result = formatContainerStatus('Proxy:healthy'); + + expect($result)->toBe('Proxy:healthy'); + }); + + it('applies headline formatting to Proxy statuses', function () { + $result = formatContainerStatus('proxy:running'); + + expect($result)->toBe('Proxy (running)'); + }); + }); + + describe('headline transformation', function () { + it('applies headline to simple lowercase status', function () { + $result = formatContainerStatus('running'); + + expect($result)->toBe('Running'); + }); + + it('applies headline to uppercase status', function () { + // headline() adds spaces between capital letters + $result = formatContainerStatus('RUNNING'); + + expect($result)->toBe('R U N N I N G'); + }); + + it('applies headline to mixed case status', function () { + // headline() adds spaces between capital letters + $result = formatContainerStatus('RuNnInG'); + + expect($result)->toBe('Ru Nn In G'); + }); + + it('applies headline to first part of colon format', function () { + // headline() adds spaces between capital letters + $result = formatContainerStatus('RUNNING:healthy'); + + expect($result)->toBe('R U N N I N G (healthy)'); + }); + }); + + describe('edge cases', function () { + it('handles empty string gracefully', function () { + $result = formatContainerStatus(''); + + expect($result)->toBe(''); + }); + + it('handles multiple colons beyond expected format', function () { + // Only first two parts should be used (or three with :excluded) + $result = formatContainerStatus('running:healthy:extra:data'); + + expect($result)->toBe('Running (healthy)'); + }); + + it('handles status with spaces in health part', function () { + $result = formatContainerStatus('running:health check failed'); + + expect($result)->toBe('Running (health check failed)'); + }); + + it('handles single colon with empty second part', function () { + $result = formatContainerStatus('running:'); + + expect($result)->toBe('Running ()'); + }); + }); + + describe('real-world scenarios', function () { + it('handles typical running healthy container', function () { + $result = formatContainerStatus('running:healthy'); + + expect($result)->toBe('Running (healthy)'); + }); + + it('handles degraded container with health issues', function () { + $result = formatContainerStatus('degraded:unhealthy'); + + expect($result)->toBe('Degraded (unhealthy)'); + }); + + it('handles excluded unhealthy container', function () { + $result = formatContainerStatus('running:unhealthy:excluded'); + + expect($result)->toBe('Running (unhealthy, excluded)'); + }); + + it('handles proxy container status', function () { + $result = formatContainerStatus('Proxy:running'); + + expect($result)->toBe('Proxy:running'); + }); + + it('handles stopped container', function () { + $result = formatContainerStatus('stopped'); + + expect($result)->toBe('Stopped'); + }); + }); +});