mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-13 20:14:45 +00:00
fix(ui): use listbox for domain redirects and key terminal canvases
Replace the service domain www-redirect native select with the shared listbox (htmlId for stable trigger IDs) and allow dropdown overflow. Key terminal target/session canvases for Livewire DOM branches, and enable terminal browser debug via ?terminal-debug= plus theme logs.
This commit is contained in:
1 parent
fff83884da
commit
78fe7ee76f
8 files changed
+77
-28
No files matched your search
@@ -8,7 +8,14 @@ import {
|
||||
} from './terminal-session-timer.js';
|
||||
import { FitAddon } from '@xterm/addon-fit';
|
||||
|
||||
const terminalDebugEnabled = import.meta.env.DEV;
|
||||
const terminalDebugParameter = new URLSearchParams(window.location.search).get('terminal-debug');
|
||||
|
||||
if (terminalDebugParameter === '1' || terminalDebugParameter === '0') {
|
||||
localStorage.setItem('coolify-terminal-debug', terminalDebugParameter);
|
||||
}
|
||||
|
||||
const terminalDebugEnabled = import.meta.env.DEV
|
||||
|| localStorage.getItem('coolify-terminal-debug') === '1';
|
||||
|
||||
const baseApplicationTerminalTheme = {
|
||||
black: '#675f70',
|
||||
@@ -393,9 +400,15 @@ export function initializeTerminalComponent() {
|
||||
|
||||
setTerminalTheme(themeName) {
|
||||
if (!applicationTerminalThemes[themeName]) {
|
||||
logTerminal('warn', '[Terminal Theme] Unknown theme', {
|
||||
requestedTheme: themeName,
|
||||
availableThemes: Object.keys(applicationTerminalThemes),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logTerminal('log', '[Terminal Theme] Applying theme', this.terminalThemeDebugSnapshot(themeName));
|
||||
|
||||
if (themeName === 'system') {
|
||||
applicationTerminalThemes.system = createSystemTerminalTheme();
|
||||
}
|
||||
@@ -417,10 +430,36 @@ export function initializeTerminalComponent() {
|
||||
this.term.options.cursorBlink = cursorBlink;
|
||||
this.term.refresh(0, Math.max(0, this.term.rows - 1));
|
||||
this.term.focus();
|
||||
logTerminal('log', '[Terminal Theme] Theme applied', this.terminalThemeDebugSnapshot(themeName));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
terminalThemeDebugSnapshot(themeName) {
|
||||
const shell = this.$el.closest('.application-console-shell');
|
||||
const viewport = this.term?.element?.querySelector('.xterm-viewport');
|
||||
const screen = this.term?.element?.querySelector('.xterm-screen');
|
||||
|
||||
return {
|
||||
requestedTheme: themeName,
|
||||
selectedTheme: this.selectedTheme,
|
||||
terminalExists: Boolean(this.term),
|
||||
terminalOpened: Boolean(this.term?.element),
|
||||
terminalActive: this.terminalActive,
|
||||
connectionState: this.connectionState,
|
||||
shellTheme: shell?.dataset.consoleTheme,
|
||||
shellBackground: shell ? getComputedStyle(shell).background : null,
|
||||
shellThemeBackground: shell ? getComputedStyle(shell).getPropertyValue('--console-theme-background') : null,
|
||||
shellThemeOpacity: shell ? getComputedStyle(shell).getPropertyValue('--console-theme-opacity') : null,
|
||||
shellPseudoBackground: shell ? getComputedStyle(shell, '::before').background : null,
|
||||
viewportBackground: viewport ? getComputedStyle(viewport).background : null,
|
||||
screenBackground: screen ? getComputedStyle(screen).background : null,
|
||||
xtermBackground: this.term?.options.theme?.background,
|
||||
xtermForeground: this.term?.options.theme?.foreground,
|
||||
xtermCursor: this.term?.options.theme?.cursor,
|
||||
};
|
||||
},
|
||||
|
||||
resetTerminal() {
|
||||
if (this.term) {
|
||||
this.$wire.dispatch('error', 'Terminal websocket connection lost. Reconnecting...');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@props([
|
||||
'id' => null,
|
||||
'htmlId' => null,
|
||||
'label' => null,
|
||||
'helper' => null,
|
||||
'required' => false,
|
||||
@@ -14,6 +15,10 @@
|
||||
'tooltip' => true,
|
||||
])
|
||||
|
||||
@php
|
||||
$triggerId = ($htmlId ?? $id).'-trigger';
|
||||
@endphp
|
||||
|
||||
<div class="w-full min-w-0">
|
||||
@if ($label)
|
||||
{{--
|
||||
@@ -23,7 +28,7 @@
|
||||
--}}
|
||||
{{-- Fixed h-4 matches the helper icon so side-by-side fields align with or without a helper. --}}
|
||||
<div class="mb-1.5 flex h-4 w-full items-center gap-1.5">
|
||||
<label for="{{ $id }}-trigger" class="mb-0! flex items-center gap-1.5 leading-4">
|
||||
<label for="{{ $triggerId }}" class="mb-0! flex items-center gap-1.5 leading-4">
|
||||
{{ $label }}
|
||||
@if ($required)
|
||||
<x-highlighted text="*" />
|
||||
@@ -52,7 +57,7 @@
|
||||
}" x-modelable="value" {{ $attributes->whereStartsWith('x-model') }}
|
||||
{{ $attributes->whereStartsWith('x-effect') }}
|
||||
@click.outside="open = false" @keydown.escape="open = false">
|
||||
<button id="{{ $id }}-trigger" type="button" class="listbox-trigger" @click="open = !open"
|
||||
<button id="{{ $triggerId }}" type="button" class="listbox-trigger" @click="open = !open"
|
||||
@disabled($disabled) {{ $attributes->whereStartsWith('x-bind:disabled') }} aria-haspopup="listbox"
|
||||
:aria-expanded="open" @if ($tooltip) :title="current" @endif>
|
||||
<span class="listbox-trigger-label" x-text="current"></span>
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
</div>
|
||||
@else
|
||||
<div wire:key="service-domains-list"
|
||||
class="application-settings-section-body is-flush mt-1 w-full scroll-mt-28 overflow-hidden">
|
||||
class="application-settings-section-body is-flush mt-1 w-full scroll-mt-28 overflow-visible">
|
||||
@foreach ($domainGroups as $appId => $rows)
|
||||
@php
|
||||
$app = collect($serviceApps)->firstWhere('id', (int) $appId);
|
||||
@@ -190,21 +190,14 @@
|
||||
<div class="flex w-full items-center gap-3 px-4 py-3">
|
||||
<span class="min-w-0 flex-1 truncate text-sm font-medium text-black dark:text-white">{{ $heading }}</span>
|
||||
@can('update', $service)
|
||||
<div class="relative flex shrink-0 items-center gap-2 px-1 py-1 text-sm text-neutral-600 dark:text-fg-dim"
|
||||
<div class="w-52 shrink-0"
|
||||
wire:loading.class="opacity-50" wire:target="serviceRedirects.{{ $appId }}">
|
||||
<span>{{ $redirectLabel }}</span>
|
||||
<x-reicon name="chevron-down" class="size-4 shrink-0"
|
||||
wire:loading.remove wire:target="serviceRedirects.{{ $appId }}" />
|
||||
<x-loading-on-button wire:loading.delay wire:target="serviceRedirects.{{ $appId }}" />
|
||||
<select id="service-domain-redirect-{{ $appId }}"
|
||||
wire:model.change="serviceRedirects.{{ $appId }}"
|
||||
wire:loading.attr="disabled" wire:target="serviceRedirects.{{ $appId }}"
|
||||
class="absolute inset-0 size-full cursor-pointer opacity-0 disabled:cursor-wait"
|
||||
aria-label="Redirect direction for {{ $heading }}">
|
||||
<option value="both">Allow www & non-www</option>
|
||||
<option value="www">Redirect to www</option>
|
||||
<option value="non-www">Redirect to non-www</option>
|
||||
</select>
|
||||
<x-forms.listbox id="serviceRedirects.{{ $appId }}"
|
||||
htmlId="service-domain-redirect-{{ $appId }}" live :options="[
|
||||
['value' => 'both', 'label' => 'Allow www & non-www'],
|
||||
['value' => 'www', 'label' => 'Redirect to www'],
|
||||
['value' => 'non-www', 'label' => 'Redirect to non-www'],
|
||||
]" />
|
||||
</div>
|
||||
@else
|
||||
<span class="shrink-0 text-sm text-neutral-600 dark:text-fg-dim">{{ $redirectLabel }}</span>
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
}
|
||||
}">
|
||||
@if ($selected_uuid === 'default')
|
||||
<div data-terminal-target-canvas
|
||||
<div wire:key="terminal-target-canvas" data-terminal-target-canvas
|
||||
class="application-console-shell relative flex h-full min-h-0 w-full items-center justify-center overflow-hidden rounded-lg p-6"
|
||||
:data-console-theme="consoleTheme"
|
||||
:style="{ '--terminal-scrollbar': themeAccents[consoleTheme] }">
|
||||
@@ -161,7 +161,7 @@
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div data-terminal-session-canvas
|
||||
<div wire:key="terminal-session-canvas" data-terminal-session-canvas
|
||||
class="application-console-shell flex h-full min-h-0 flex-col overflow-hidden rounded-lg p-3 sm:p-6"
|
||||
:data-console-theme="consoleTheme"
|
||||
:style="{ '--terminal-scrollbar': themeAccents[consoleTheme] }">
|
||||
|
||||
@@ -183,11 +183,12 @@ it('mounts the realtime terminal utilities in local development compose files',
|
||||
'maxio dev compose' => 'docker-compose-maxio.dev.yml',
|
||||
]);
|
||||
|
||||
it('keeps terminal browser logging restricted to Vite development mode', function () {
|
||||
it('keeps terminal browser logging restricted to development or explicit diagnostics', function () {
|
||||
$terminalClient = file_get_contents(base_path('resources/js/terminal.js'));
|
||||
|
||||
expect($terminalClient)
|
||||
->toContain('const terminalDebugEnabled = import.meta.env.DEV;')
|
||||
->toContain('const terminalDebugEnabled = import.meta.env.DEV')
|
||||
->toContain("localStorage.getItem('coolify-terminal-debug') === '1'")
|
||||
->toContain("logTerminal('log', '[Terminal] WebSocket connection established.');")
|
||||
->not->toContain("console.log('[Terminal] WebSocket connection established. Cool cool cool cool cool cool.');");
|
||||
});
|
||||
|
||||
@@ -104,9 +104,11 @@ it('groups configured domains with their service redirect and excludes services
|
||||
|
||||
expect($html)
|
||||
->toContain("service-domain-group-{$this->apiApp->id}")
|
||||
->toContain("service-domain-redirect-{$this->apiApp->id}")
|
||||
->toContain("wire:model.change=\"serviceRedirects.{$this->apiApp->id}\"")
|
||||
->toContain("wire:target=\"serviceRedirects.{$this->apiApp->id}\"")
|
||||
->toContain("id=\"service-domain-redirect-{$this->apiApp->id}-trigger\"")
|
||||
->toContain("serviceRedirects.{$this->apiApp->id}")
|
||||
->toContain('class="listbox-trigger"')
|
||||
->toContain('application-settings-section-body is-flush mt-1 w-full scroll-mt-28 overflow-visible')
|
||||
->not->toContain("<select id=\"service-domain-redirect-{$this->apiApp->id}\"")
|
||||
->not->toContain("service-domain-redirect-toggle-{$this->apiApp->id}")
|
||||
->not->toContain("service-domain-group-{$this->webApp->id}")
|
||||
->and(substr_count($html, '2 domains'))->toBe(1)
|
||||
|
||||
@@ -20,7 +20,7 @@ it('shows a centered themed target canvas before loading xterm', function () {
|
||||
->toContain('data-terminal-target-canvas')
|
||||
->toContain('items-center justify-center')
|
||||
->toContain(':data-console-theme="consoleTheme"')
|
||||
->toContain("@else\n <div data-terminal-session-canvas");
|
||||
->toContain("@else\n <div wire:key=\"terminal-session-canvas\" data-terminal-session-canvas");
|
||||
});
|
||||
|
||||
it('loads targets inside the themed session picker with an accent scrollbar', function () {
|
||||
@@ -54,6 +54,14 @@ it('uses the same padded themed canvas for the active terminal session', functio
|
||||
->toMatch('/\.terminal-session-panel\s*\{[^}]*box-shadow:\s*none;/s');
|
||||
});
|
||||
|
||||
it('keeps pre-connection and connected terminal canvases in distinct Livewire DOM branches', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/terminal/index.blade.php'));
|
||||
|
||||
expect($view)
|
||||
->toContain('wire:key="terminal-target-canvas"')
|
||||
->toContain('wire:key="terminal-session-canvas"');
|
||||
});
|
||||
|
||||
it('opens the global terminal outside Livewire navigation like resource terminals', function () {
|
||||
$navbar = file_get_contents(resource_path('views/components/navbar.blade.php'));
|
||||
|
||||
|
||||
@@ -111,8 +111,9 @@ it('renders an automatically added redirect counterpart without reloading', func
|
||||
$page = visit("{$base}/domains")
|
||||
->assertSee('https://web.example.com');
|
||||
|
||||
$selector = '#service-domain-redirect-'.$this->serviceApplication->id;
|
||||
$page->select($selector, 'www')
|
||||
$selector = '#service-domain-redirect-'.$this->serviceApplication->id.'-trigger';
|
||||
$page->click($selector)
|
||||
->click('Redirect to www')
|
||||
->wait(3);
|
||||
|
||||
$this->serviceApplication->refresh();
|
||||
|
||||
Reference in new issue
Block a user