refactor(application): extract status and internal access components

Move internal hostname loading and breadcrumb status into dedicated
Livewire components with live refresh. Merge public/internal access
into one Access section, bind Enter to save on the unsaved bar, and
drop x-teleport wrappers from popup and global search.
This commit is contained in:
Andras Bacsai
2026-08-06 14:29:25 +02:00
parent a6ab360ac2
commit 0be13113d1
17 changed files with 259 additions and 211 deletions
@@ -1,6 +1,7 @@
<?php
use App\Livewire\Project\Application\Heading as ApplicationHeading;
use App\Livewire\Project\Application\Status as ApplicationStatus;
use App\Models\Application;
use App\Models\InstanceSettings;
use App\Models\Project;
@@ -166,6 +167,21 @@ it('keeps activeRouteName when request is not an application page route', functi
->assertSet('activeRouteName', 'project.application.webhooks');
});
it('refreshes the breadcrumb application status after it changes', function () {
$this->actingAs($this->admin);
session(['currentTeam' => $this->team]);
$component = Livewire::test(ApplicationStatus::class, ['application' => $this->application])
->assertSee('Running');
$this->application->update(['status' => 'exited']);
$component
->call('refreshStatus')
->assertSee('Stopped')
->assertDontSee('Running');
});
it('uses app-tab-active utility for resource heading active styles', function () {
$utilities = file_get_contents(resource_path('css/utilities.css'));
@@ -3,11 +3,19 @@
it('shows internal Docker access details in application general settings', function () {
$generalSettings = file_get_contents(resource_path('views/livewire/project/application/general.blade.php'));
$generalComponent = file_get_contents(app_path('Livewire/Project/Application/General.php'));
$internalAccessSettings = file_get_contents(resource_path('views/livewire/project/application/internal-access.blade.php'));
$internalAccessComponent = file_get_contents(app_path('Livewire/Project/Application/InternalAccess.php'));
$configurationSidebar = file_get_contents(resource_path('views/components/application/configuration-sidebar.blade.php'));
expect($generalSettings)
->toContain('id="access-section" title="Access"')
->toContain('<h3 class="mb-4 text-sm font-semibold text-black dark:text-fg">Public access</h3>')
->toContain('<livewire:project.application.internal-access')
->not->toContain('wire:init="loadCurrentInternalHostname"')
->and($internalAccessSettings)
->toContain('id="internal-access-section"')
->toContain('title="Internal access"')
->toContain('<h3 class="mb-4 text-sm font-semibold text-black dark:text-fg">Internal access</h3>')
->not->toContain('<x-application.settings-section')
->toContain('Internal hostname')
->toContain('Docker network')
->toContain('Exposed ports')
@@ -18,12 +26,14 @@ it('shows internal Docker access details in application general settings', funct
->not->toContain('Changes with each deployment')
->toContain("window.scrollToSettingsSection?.('networking-section')")
->and($generalComponent)
->not->toContain('public function loadCurrentInternalHostname(): void')
->and($internalAccessComponent)
->toContain('public ?string $currentInternalHostname = null;')
->toContain('public function loadCurrentInternalHostname(): void')
->toContain('getCurrentApplicationContainerStatus(')
->toContain("data_get(\$currentContainer, 'Names')")
->and($configurationSidebar)
->toContain("['id' => 'internal-access-section', 'label' => 'Internal access']");
->toContain("['id' => 'access-section', 'label' => 'Access']");
});
it('does not show the internal access section for Docker Compose applications', function () {
@@ -33,5 +43,7 @@ it('does not show the internal access section for Docker Compose applications',
expect($generalSettings)
->toContain("@if (\$buildPack !== 'dockercompose')")
->and($configurationSidebar)
->toContain("\$isComposeApp ? null : ['id' => 'internal-access-section', 'label' => 'Internal access']");
->toContain("['id' => 'access-section', 'label' => 'Access']")
->not->toContain("['id' => 'public-access-section', 'label' => 'Public access']")
->not->toContain("['id' => 'internal-access-section', 'label' => 'Internal access']");
});
@@ -12,3 +12,16 @@ it('shows a loading overlay while resource selection steps load', function () {
->toContain('runPaletteTransition')
->toContain('Loading…');
});
it('uses a single Alpine result renderer for every command palette result type', function () {
$view = file_get_contents(resource_path('views/livewire/global-search.blade.php'));
expect($view)
->not->toContain('Create mode (server-rendered path)')
->not->toContain('!$wire.isCreateMode')
->toContain("<!-- Command palette -->\n <div x-show=\"modalOpen\"")
->not->toContain("<!-- Command palette -->\n <template x-teleport=\"body\">")
->toContain("<div wire:ignore>\n <template x-if=\"searchQuery.length")
->toContain('x-for="(result, index) in searchResults"')
->toContain('x-for="[categoryName, items] in Object.entries(groupedCreatableItems)"');
});
@@ -88,7 +88,7 @@ it('supports timed compact popup notifications', function () {
->toContain("localStorage.setItem(this.storageKey, 'compact')")
->toContain("localStorage.setItem(this.storageKey, 'icon')")
->toContain('localStorage.removeItem(key)')
->toContain('<template x-teleport="body">')
->not->toContain('<template x-teleport="body">')
->toContain('compact = true')
->toContain('@click="restore()"')
->toContain('@click.stop="minimizeToIcon()"')
@@ -113,6 +113,22 @@ it('refreshes configuration changes when the event is received', function () {
->assertSee('Build command');
});
it('shows domain changes when the domain page dispatches a configuration change', function () {
$application = configurationCheckerApplication($this->environment);
markConfigurationCheckerApplicationDeployed($application);
$component = Livewire::test(ConfigurationChecker::class, ['resource' => $application->refresh()])
->assertSet('isConfigurationChanged', false);
$application->update(['fqdn' => 'https://changed.example.com']);
$component
->dispatch('configurationChanged')
->assertSet('isConfigurationChanged', true)
->assertSee('Domains')
->assertSee('https://changed.example.com');
});
it('shows an unapplied configuration warning after a directory mount is added', function () {
$application = configurationCheckerApplication($this->environment);
markConfigurationCheckerApplicationDeployed($application);
@@ -36,6 +36,17 @@ test('unsaved bar delays show and hides while loading to avoid instant-save flas
->toContain('delay-0');
});
test('unsaved bar saves with enter and shows the shortcut on the save button', function () {
$contents = file_get_contents(resource_path('views/components/unsaved-bar.blade.php'));
expect($contents)
->toContain('@keydown.enter.window')
->toContain("classList.contains('is-dirty')")
->toContain('$wire.{{ $action }}()')
->toContain('<kbd')
->toContain('Enter</kbd>');
});
test('unsaved bar stays above floating notifications so save actions remain accessible', function () {
$unsavedBar = file_get_contents(resource_path('views/components/unsaved-bar.blade.php'));
$popup = file_get_contents(resource_path('views/components/popup-small.blade.php'));
+10 -1
View File
@@ -38,12 +38,21 @@ it('renders service container statuses as shared status badges', function () {
it('uses bordered status badges in the top breadcrumb', function () {
$breadcrumb = file_get_contents(resource_path('views/components/top-breadcrumb.blade.php'));
$applicationStatus = file_get_contents(resource_path('views/livewire/project/application/status.blade.php'));
$borderedBadgeClasses = 'rounded-full border border-neutral-200 bg-neutral-100';
expect(substr_count($breadcrumb, $borderedBadgeClasses))->toBe(4)
expect(substr_count($breadcrumb.$applicationStatus, $borderedBadgeClasses))->toBe(4)
->and(substr_count($breadcrumb, 'rounded-full bg-neutral-100'))->toBe(0);
});
it('renders application status through a reactive livewire component', function () {
$breadcrumb = file_get_contents(resource_path('views/components/top-breadcrumb.blade.php'));
expect($breadcrumb)
->toContain('<livewire:project.application.status')
->not->toContain('$applicationStatus = str($currentApplication->status');
});
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'));