diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index 3fa063298..bb3991f22 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -29,13 +29,15 @@ class ExecuteContainerCommand extends Component public bool $isConnecting = false; + public bool $containersLoaded = false; + protected $rules = [ 'server' => 'required', 'container' => 'required', 'command' => 'required', ]; - public function mount() + public function mount(): void { $this->parameters = get_route_parameters(); $this->containers = collect(); @@ -52,7 +54,6 @@ class ExecuteContainerCommand extends Component $this->servers = $this->servers->push($server); } } - $this->loadContainers(); } elseif (data_get($this->parameters, 'database_uuid')) { $this->type = 'database'; $resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id')); @@ -64,7 +65,6 @@ class ExecuteContainerCommand extends Component if ($this->resource->destination->server->isFunctional()) { $this->servers = $this->servers->push($this->resource->destination->server); } - $this->loadContainers(); } elseif (data_get($this->parameters, 'service_uuid')) { $this->type = 'service'; $this->resource = Service::ownedByCurrentTeam()->where('uuid', $this->parameters['service_uuid'])->firstOrFail(); @@ -72,18 +72,22 @@ class ExecuteContainerCommand extends Component if ($this->resource->server->isFunctional()) { $this->servers = $this->servers->push($this->resource->server); } - $this->loadContainers(); } elseif (data_get($this->parameters, 'server_uuid')) { $this->type = 'server'; $this->resource = Server::ownedByCurrentTeam()->where('uuid', $this->parameters['server_uuid'])->firstOrFail(); $this->authorize('view', $this->resource); $this->servers = $this->servers->push($this->resource); + $this->containersLoaded = true; } $this->servers = $this->servers->sortByDesc(fn ($server) => $server->isTerminalEnabled()); } - public function loadContainers() + public function loadContainers(): void { + if ($this->containersLoaded) { + return; + } + foreach ($this->servers as $server) { if (data_get($this->parameters, 'application_uuid')) { if ($server->isSwarm()) { @@ -145,7 +149,10 @@ class ExecuteContainerCommand extends Component if ($this->containers->count() === 1) { $this->selected_container = data_get($this->containers->first(), 'container.Names'); + $this->connectToContainer(); } + + $this->containersLoaded = true; } public function updatedSelectedContainer() diff --git a/app/Livewire/Project/Shared/Terminal.php b/app/Livewire/Project/Shared/Terminal.php index 3b704d007..f6145807e 100644 --- a/app/Livewire/Project/Shared/Terminal.php +++ b/app/Livewire/Project/Shared/Terminal.php @@ -17,6 +17,8 @@ class Terminal extends Component public bool $isTerminalConnected = false; + public bool $autoStart = false; + public string $variant = 'default'; private function checkShellAvailability(Server $server, string $container): bool diff --git a/resources/css/app.css b/resources/css/app.css index c61458fe8..400b4a9a0 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -500,11 +500,6 @@ html:not(.dark) .application-console-shell[data-console-theme="system"] .applica backdrop-filter: none; } -html:not(.dark) .application-console-shell[data-console-theme="system"] .application-console-block::before { - background: transparent; - backdrop-filter: none; -} - html:not(.dark) .application-console-shell[data-console-theme="system"] .application-console-header [class*="text-white"] { color: #52525b !important; } @@ -678,7 +673,7 @@ body.terminal-is-fullscreen .terminal-fullscreen-shell { inset: 0 !important; z-index: 100000 !important; width: 100vw !important; - height: 100dvh !important; + height: auto !important; max-width: none !important; margin: 0 !important; overflow: hidden !important; @@ -707,25 +702,111 @@ body.terminal-is-fullscreen .terminal-fullscreen-shell [data-terminal-mobile-too backdrop-filter: blur(20px); } +.terminal-loading-label { + font-size: 0.875rem; + font-weight: 500; + color: rgb(255 255 255 / 0.75); +} + +html:not(.dark) .application-console-shell[data-console-theme="system"] .terminal-loading-label { + color: #52525b; +} + +.terminal-session-expiry { + font-size: 0.75rem; + font-weight: 500; + color: rgb(255 255 255 / 0.6); +} + +html:not(.dark) .application-console-shell[data-console-theme="system"] .terminal-session-expiry, +html:not(.dark) .terminal-fullscreen-shell[data-console-theme="system"] .terminal-session-expiry { + color: #52525b; +} + +.terminal-target-picker { + color: rgb(255 255 255 / 0.75); + background: rgb(0 0 0 / 0.18); + border-color: rgb(255 255 255 / 0.1); + backdrop-filter: blur(16px); +} + +.terminal-session-panel { + border: 0; + border-radius: 0.75rem; + background: transparent; + box-shadow: none; +} + +html:not(.dark) .application-console-shell[data-console-theme="system"] .terminal-session-panel { + background: transparent; +} + +.terminal-target-list { + scrollbar-width: thin; + scrollbar-color: var(--terminal-scrollbar, rgb(255 255 255 / 0.3)) transparent; +} + +.terminal-target-list::-webkit-scrollbar { + width: 8px; +} + +.terminal-target-list::-webkit-scrollbar-thumb { + border: 2px solid transparent; + border-radius: 9999px; + background: var(--terminal-scrollbar, rgb(255 255 255 / 0.3)); + background-clip: padding-box; +} + +.terminal-target-list::-webkit-scrollbar-track { + background: transparent; +} + +html:not(.dark) .application-console-shell[data-console-theme="system"] .terminal-target-picker { + color: #52525b; + background: #fff; + border-color: #d4d4d8; + box-shadow: 0 18px 50px rgb(0 0 0 / 0.18); +} + +html:not(.dark) .application-console-shell[data-console-theme="system"] .terminal-target-picker [class*="text-white"] { + color: #52525b !important; +} + +html:not(.dark) .application-console-shell[data-console-theme="system"] .terminal-target-picker input { + color: #27272a !important; + background: #f4f4f5 !important; + border-color: #d4d4d8 !important; +} + +html:not(.dark) .application-console-shell[data-console-theme="system"] .terminal-target-picker input::placeholder { + color: #71717a !important; +} + +html:not(.dark) .application-console-shell[data-console-theme="system"] .terminal-target-picker button:hover { + color: #18181b !important; + background: #f4f4f5; +} + +html:not(.dark) .application-console-shell[data-console-theme="system"] .terminal-theme-trigger { + color: #52525b; +} + +html:not(.dark) .application-console-shell[data-console-theme="system"] .terminal-session-target-trigger { + color: #52525b; +} + +html:not(.dark) .application-console-shell[data-console-theme="system"] .terminal-theme-trigger:hover, +html:not(.dark) .application-console-shell[data-console-theme="system"] .terminal-session-target-trigger:hover { + color: #18181b; + background: rgb(0 0 0 / 0.06); +} + .application-console-block { - /* Keep backdrop blur on a pseudo-element so position:fixed descendants - (terminal fullscreen) are not trapped by a backdrop-filter containing block. */ position: relative; z-index: 1; isolation: isolate; } -.application-console-block::before { - position: absolute; - z-index: -1; - inset: 0; - border-radius: inherit; - background: rgb(0 0 0 / 0.56); - backdrop-filter: blur(10px); - content: ""; - pointer-events: none; -} - .terminal-fullscreen-btn { display: flex; width: 1.75rem; @@ -733,10 +814,9 @@ body.terminal-is-fullscreen .terminal-fullscreen-shell [data-terminal-mobile-too align-items: center; justify-content: center; border-radius: 0.375rem; - border: 1px solid rgb(255 255 255 / 0.08); - background: rgb(0 0 0 / 0.55); - color: rgb(255 255 255 / 0.65); - backdrop-filter: blur(8px); + border: 0; + background: transparent; + color: var(--terminal-scrollbar, rgb(255 255 255 / 0.65)); transition: background-color 150ms ease, color 150ms ease, @@ -744,8 +824,8 @@ body.terminal-is-fullscreen .terminal-fullscreen-shell [data-terminal-mobile-too } .terminal-fullscreen-btn:hover { - background: rgb(255 255 255 / 0.08); - color: rgb(255 255 255 / 0.95); + background: color-mix(in srgb, var(--terminal-scrollbar, #fff) 18%, transparent); + color: var(--terminal-scrollbar, #fff); } /* diff --git a/resources/js/terminal.js b/resources/js/terminal.js index 26e32bb6f..25867e500 100644 --- a/resources/js/terminal.js +++ b/resources/js/terminal.js @@ -152,6 +152,7 @@ export function initializeTerminalComponent() { return { fullscreen: false, terminalActive: false, + starting: false, message: '(connection closed)', term: null, fitAddon: null, @@ -203,6 +204,7 @@ export function initializeTerminalComponent() { : 'system', init() { + this.starting = this.$el.dataset.autoStart === 'true'; this.themeObserver = new MutationObserver(() => { if (this.selectedTheme === 'system') { applicationTerminalThemes.system = createSystemTerminalTheme(); @@ -676,6 +678,7 @@ export function initializeTerminalComponent() { } if (event.data === 'pty-ready') { + this.starting = false; if (!this.term._initialized) { this.term.open(document.getElementById('terminal')); this.term._initialized = true; @@ -715,6 +718,7 @@ export function initializeTerminalComponent() { // Notify parent component that terminal is connected this.$wire.dispatch('terminalConnected'); } else if (event.data === 'unprocessable') { + this.starting = false; if (this.term) this.term.reset(); this.terminalActive = false; this.lastSentCommand = null; @@ -724,6 +728,7 @@ export function initializeTerminalComponent() { // Notify parent component that terminal connection failed this.$wire.dispatch('terminalDisconnected'); } else if (event.data === 'pty-exited') { + this.starting = false; this.exitFullscreen(); this.mobileToolbarCollapsed = false; this.terminalActive = false; diff --git a/resources/views/components/terminal/theme-selector.blade.php b/resources/views/components/terminal/theme-selector.blade.php new file mode 100644 index 000000000..18f0585fd --- /dev/null +++ b/resources/views/components/terminal/theme-selector.blade.php @@ -0,0 +1,33 @@ +@props(['themes', 'themeNames', 'themeAccents']) + +
+ {{ $isLoadingContainers ? 'Finding available servers and containers…' : 'Choose a server or container. The terminal will open after you select a target.' }} +
+ @if (! $isLoadingContainers && $servers->isNotEmpty()) +