fix(ui): polish create flows and terminal navigation

Open the global terminal outside Livewire navigation, clarify that
image tag and SHA256 digest are mutually exclusive, inset the GitLab
empty state, and show loading on Dockerfile create. Add regression tests.
This commit is contained in:
Andras Bacsai 2026-08-07 17:54:50 +02:00
parent c07f66cb5e
commit fff83884da
9 changed files with 62 additions and 12 deletions

View file

@ -89,7 +89,7 @@ class="{{ request()->is('project/*') || request()->is('projects') ? 'menu-item m
</li>
@can('canAccessTerminal')
<li>
<a title="Terminal" {{ wireNavigate() }}
<a title="Terminal"
class="{{ request()->is('terminal*') ? 'menu-item-active menu-item' : 'menu-item' }}"
:class="collapsed && 'lg:justify-center lg:px-0'" href="{{ route('terminal') }}">
<x-reicon name="browser-terminal" class="menu-item-icon" />

View file

@ -14,9 +14,14 @@ class="mt-8 w-full max-w-[920px] lg:mt-3">
placeholder="nginx, ghcr.io/user/app:v1.2.3, or nginx:stable@sha256:…"
helper="Paste a complete image reference, or enter a name and use one of the optional fields below."
required autofocus />
<div class="grid gap-4 sm:grid-cols-2">
<div class="grid gap-3 sm:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] sm:items-end"
aria-label="Tag and SHA256 digest are mutually exclusive">
<x-forms.input id="imageTag" label="Tag" placeholder="latest"
helper="Use a mutable tag such as latest or v1.2.3." />
<div
class="flex items-center justify-center text-xs font-semibold text-neutral-400 sm:h-9 dark:text-fg-faint">
<span>OR</span>
</div>
<x-forms.input id="imageSha256" label="SHA256 digest"
placeholder="59e02939b1bf39f16c93138a28727aec…"
helper="Use the 64-character digest without the sha256: prefix." />

View file

@ -7,15 +7,17 @@
<p>Connect a GitLab App before selecting a private repository.</p>
</div>
</div>
<x-empty title="No GitLab Apps"
description="Create an app to grant Coolify access to selected repositories."
icon-name="sources">
<x-slot:contents>
<x-modal-input buttonTitle="+ Add GitLab App" title="New GitLab App" closeOutside="false">
<livewire:source.gitlab.create />
</x-modal-input>
</x-slot:contents>
</x-empty>
<div class="application-settings-section-body">
<x-empty title="No GitLab Apps"
description="Create an app to grant Coolify access to selected repositories."
icon-name="sources">
<x-slot:contents>
<x-modal-input buttonTitle="+ Add GitLab App" title="New GitLab App" closeOutside="false">
<livewire:source.gitlab.create />
</x-modal-input>
</x-slot:contents>
</x-empty>
</div>
</section>
@elseif ($current_step === 'gitlab_apps')
<section class="application-settings-section">

View file

@ -6,7 +6,7 @@
<h2>Dockerfile</h2>
<p>Create an application directly from a Dockerfile without connecting a Git repository.</p>
</div>
<x-forms.button type="submit" isHighlighted>Create application</x-forms.button>
<x-forms.button type="submit" wire:target="submit" isHighlighted>Create application</x-forms.button>
</div>
<div class="application-settings-section-body p-0!">
<x-forms.textarea useMonacoEditor monacoEditorLanguage="dockerfile" rows="20"

View file

@ -0,0 +1,9 @@
<?php
it('shows that image tag and digest are mutually exclusive', function () {
$view = file_get_contents(resource_path('views/livewire/project/new/docker-image.blade.php'));
expect($view)
->toContain('aria-label="Tag and SHA256 digest are mutually exclusive"')
->toContain('>OR</span>');
});

View file

@ -46,6 +46,12 @@
});
describe('GitLab App authorization', function () {
test('empty gitlab app state is inset from the section edges', function () {
$view = file_get_contents(resource_path('views/livewire/project/new/gitlab-private-repository.blade.php'));
expect($view)->toContain("<div class=\"application-settings-section-body\">\n <x-empty title=\"No GitLab Apps\"");
});
test('unrelated users cannot inspect system-wide source secrets in the component payload', function () {
$otherTeam = Team::factory()->create();
$systemWideSource = GitlabApp::create([

View file

@ -15,6 +15,18 @@
->not->toContain('<x-loading text="Loading containers" />');
});
it('provides opt-in diagnostics for connected terminal theme changes', function () {
$terminalClient = file_get_contents(resource_path('js/terminal.js'));
expect($terminalClient)
->toContain('terminal-debug')
->toContain("'[Terminal Theme] Applying theme'")
->toContain("'[Terminal Theme] Theme applied'")
->toContain('requestedTheme: themeName')
->toContain('shellTheme: shell?.dataset.consoleTheme')
->toContain("getComputedStyle(shell, '::before').background");
});
it('starts a single discovered resource container without waiting for a missed browser event', function () {
$terminalComponent = file_get_contents(app_path('Livewire/Project/Shared/ExecuteContainerCommand.php'));
$terminalView = file_get_contents(resource_path('views/livewire/project/shared/execute-container-command.blade.php'));

View file

@ -0,0 +1,7 @@
<?php
it('shows a loading indicator while creating the application', function () {
$view = file_get_contents(resource_path('views/livewire/project/new/simple-dockerfile.blade.php'));
expect($view)->toContain('<x-forms.button type="submit" wire:target="submit" isHighlighted>');
});

View file

@ -54,6 +54,15 @@
->toMatch('/\.terminal-session-panel\s*\{[^}]*box-shadow:\s*none;/s');
});
it('opens the global terminal outside Livewire navigation like resource terminals', function () {
$navbar = file_get_contents(resource_path('views/components/navbar.blade.php'));
expect($navbar)
->toContain('<a title="Terminal"')
->toContain('href="{{ route(\'terminal\') }}"')
->not->toMatch('/<a title="Terminal"[^>]*wireNavigate\(\)/s');
});
it('uses floating rounded controls instead of the legacy terminal header bar', function () {
$view = file_get_contents(resource_path('views/livewire/terminal/index.blade.php'));