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
+1 -1
View File
@@ -89,7 +89,7 @@
</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" />
@@ -14,9 +14,14 @@
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." />
@@ -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">
@@ -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"
@@ -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>');
});
@@ -46,6 +46,12 @@ beforeEach(function () {
});
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([
@@ -15,6 +15,18 @@ it('renders the resource terminal shell while containers are discovered', functi
->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'));
@@ -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>');
});
+9
View File
@@ -54,6 +54,15 @@ it('uses the same padded themed canvas for the active terminal session', functio
->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'));