diff --git a/app/Livewire/Project/Application/Backup/Index.php b/app/Livewire/Project/Application/Backup/Index.php index 130396a9b..fe7e2eac5 100644 --- a/app/Livewire/Project/Application/Backup/Index.php +++ b/app/Livewire/Project/Application/Backup/Index.php @@ -31,7 +31,7 @@ class Index extends Component public function render(): View { $backups = ScheduledVolumeBackup::query() - ->with(['backupable', 'latestExecution']) + ->with(['backupable', 'latestExecution', 's3']) ->withCount('executions') ->forApplication($this->application) ->latest() diff --git a/app/Livewire/Project/Service/VolumeBackup/Index.php b/app/Livewire/Project/Service/VolumeBackup/Index.php index ad6b7d6c2..a56653597 100644 --- a/app/Livewire/Project/Service/VolumeBackup/Index.php +++ b/app/Livewire/Project/Service/VolumeBackup/Index.php @@ -31,7 +31,7 @@ class Index extends Component public function render(): View { $backups = ScheduledVolumeBackup::query() - ->with(['backupable.resource', 'latestExecution']) + ->with(['backupable.resource', 'latestExecution', 's3']) ->withCount('executions') ->forService($this->service) ->latest() 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/Storages/All.php b/app/Livewire/Project/Shared/Storages/All.php index 54175d21a..15a154e67 100644 --- a/app/Livewire/Project/Shared/Storages/All.php +++ b/app/Livewire/Project/Shared/Storages/All.php @@ -28,7 +28,7 @@ class All extends Component /** * Precomputed per-volume backup badge/link data. * - * @var array + * @var array */ public array $volumeBackupMeta = []; @@ -241,6 +241,7 @@ class All extends Component $this->volumeBackupMeta[(int) $storage->id] = [ 'enabled' => $enabled, + 's3' => $enabled && (bool) $backup?->save_s3, 'url' => $url, ]; } 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 81f63b1b9..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); } /* @@ -2149,7 +2229,7 @@ input[type="search"]::-webkit-search-results-decoration { } .backup-table-grid { - grid-template-columns: minmax(10rem, 1.7fr) 6rem minmax(7rem, 0.8fr) 7.5rem minmax(8rem, 1fr) 5rem; + grid-template-columns: minmax(10rem, 1.7fr) 6rem minmax(7rem, 0.8fr) 7.5rem 6.5rem minmax(8rem, 1fr) 5rem; } /* Persistent storage volumes: Name | Source | Destination | [PR suffix] | Backup | [Actions] */ @@ -2383,11 +2463,12 @@ input[type="search"]::-webkit-search-results-decoration { @media (max-width: 900px) { .backup-table-grid { - grid-template-columns: minmax(10rem, 1.5fr) 6rem 7.5rem minmax(8rem, 1fr); + grid-template-columns: minmax(10rem, 1.5fr) 6rem 7.5rem 6.5rem; } .backup-table-grid > :nth-child(3), - .backup-table-grid > :nth-child(6) { + .backup-table-grid > :nth-child(6), + .backup-table-grid > :nth-child(7) { display: none; } @@ -2443,11 +2524,10 @@ input[type="search"]::-webkit-search-results-decoration { } .backup-table-grid { - grid-template-columns: minmax(0, 1fr) 7.5rem; + grid-template-columns: minmax(0, 1fr) 7.5rem 6.5rem; } - .backup-table-grid > :nth-child(2), - .backup-table-grid > :nth-child(5) { + .backup-table-grid > :nth-child(2) { display: none; } 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/helper.blade.php b/resources/views/components/helper.blade.php index 72ac38d78..6b90b5929 100644 --- a/resources/views/components/helper.blade.php +++ b/resources/views/components/helper.blade.php @@ -73,8 +73,10 @@ const viewportBottom = viewportTop + viewportHeight; const availableWidth = Math.max(0, viewportWidth - padding * 2); const availableHeight = Math.max(0, viewportHeight - padding * 2); + const maxPopupWidth = 320; + const popupWidth = Math.min(maxPopupWidth, availableWidth); - this.style = `max-width: ${availableWidth}px; max-height: ${availableHeight}px; overflow-y: auto;`; + this.style = `max-width: ${popupWidth}px; max-height: ${availableHeight}px; overflow-y: auto;`; const triggerRect = trigger.getBoundingClientRect(); const popupRect = popup.getBoundingClientRect(); @@ -120,7 +122,7 @@ x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" :style="style" - class="info-helper-popup fixed z-[9999] w-max max-w-[min(20rem,calc(100vw-2rem))]" + class="info-helper-popup fixed z-[9999] w-max max-w-[min(20rem,calc(100vw-2rem))] whitespace-normal" @mouseenter="cancelHide()" @mouseleave="hide()" @focusout="closeWhenFocusLeaves()" @click.stop>
{!! $helper !!} 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']) + +
+ + +
+ @foreach ($themes as $theme) + + @endforeach +
+
diff --git a/resources/views/livewire/project/application/backup/index.blade.php b/resources/views/livewire/project/application/backup/index.blade.php index 8fd104b28..12660d527 100644 --- a/resources/views/livewire/project/application/backup/index.blade.php +++ b/resources/views/livewire/project/application/backup/index.blade.php @@ -178,6 +178,7 @@ Type Schedule Status + S3 Last run Executions
@@ -212,6 +213,10 @@ {{ $backup->targetType() }} {{ $backup->frequency }} + + + {{ $latestExecution?->finished_at?->diffForHumans() ?? ($status === 'running' ? 'Running now' : 'Never') }} diff --git a/resources/views/livewire/project/service/resource-card.blade.php b/resources/views/livewire/project/service/resource-card.blade.php index 43feffcd6..be026313a 100644 --- a/resources/views/livewire/project/service/resource-card.blade.php +++ b/resources/views/livewire/project/service/resource-card.blade.php @@ -39,17 +39,10 @@ @endif @if ($isApplication && $resource->fqdn) -
+
{{ $resource->fqdn }} - @can('update', $service) - - - - @endcan
@endif
@@ -63,6 +56,14 @@ @endif + @if ($isApplication && $resource->fqdn) + @can('update', $service) + + + + @endcan + @endif @@ -109,6 +110,14 @@ @endif + @if ($isApplication && $resource->fqdn) + @can('update', $service) + + + + @endcan + @endif diff --git a/resources/views/livewire/project/service/volume-backup/index.blade.php b/resources/views/livewire/project/service/volume-backup/index.blade.php index 6c531d2da..78c7d969b 100644 --- a/resources/views/livewire/project/service/volume-backup/index.blade.php +++ b/resources/views/livewire/project/service/volume-backup/index.blade.php @@ -179,6 +179,7 @@ Type Schedule Status + S3 Last run Executions @@ -213,6 +214,10 @@ {{ $backup->targetType() }} {{ $backup->frequency }} + + + {{ $latestExecution?->finished_at?->diffForHumans() ?? ($status === 'running' ? 'Running now' : 'Never') }} diff --git a/resources/views/livewire/project/shared/destination.blade.php b/resources/views/livewire/project/shared/destination.blade.php index d8c74a516..720e85bdc 100644 --- a/resources/views/livewire/project/shared/destination.blade.php +++ b/resources/views/livewire/project/shared/destination.blade.php @@ -1,14 +1,6 @@
@if ($resource->getMorphClass() === 'App\Models\Application') @php - $primaryStatus = str($resource->realStatus()); - $primaryStatusType = match (true) { - $primaryStatus->startsWith('running') => 'success', - $primaryStatus->startsWith('exited') => 'error', - $primaryStatus->startsWith(['starting', 'restarting']) => 'warning', - default => 'neutral', - }; - $primaryStatusLabel = $primaryStatus->before(':')->headline()->value() ?: 'Unknown'; $additionalDestinations = $resource->additional_networks; $hasAdditionalDestinations = $additionalDestinations->isNotEmpty(); @endphp @@ -41,21 +33,13 @@
- @if ($primaryStatus->startsWith('running')) - - @elseif ($primaryStatus->startsWith(['starting', 'restarting'])) - - @elseif ($primaryStatus->startsWith('exited')) - - @else - - @endif + @if ($hasAdditionalDestinations) Deploy - @if ($primaryStatus->startsWith('running')) + @if (str($resource->status)->startsWith('running')) Stop diff --git a/resources/views/livewire/project/shared/execute-container-command.blade.php b/resources/views/livewire/project/shared/execute-container-command.blade.php index 887876813..25e9b452f 100644 --- a/resources/views/livewire/project/shared/execute-container-command.blade.php +++ b/resources/views/livewire/project/shared/execute-container-command.blade.php @@ -1,4 +1,4 @@ -
+
{{ data_get_str($resource, 'name')->limit(10) }} > Terminal | Coolify @@ -18,7 +18,7 @@ @php $consoleUnavailable = ($type === 'server' && (! $servers->first()->isTerminalEnabled() || ! $servers->first()->isFunctional())) - || ($type !== 'server' && $containers->isEmpty()); + || ($type !== 'server' && $containersLoaded && $containers->isEmpty()); $consoleThemes = [ ['key' => 'system', 'name' => 'System', 'background' => 'linear-gradient(135deg, #ffffff 0 50%, #121214 50% 100%)', 'accent' => '#8C8E9C'], ['key' => 'shadows-midnight', 'name' => 'Midnight', 'background' => 'linear-gradient(135deg, #2a3b4c, rgba(42, 59, 76, 0.4))', 'accent' => '#6d7a7c'], @@ -70,11 +70,15 @@ @else
-
+
+ class="terminal-session-toolbar absolute top-3 right-3 left-3 z-20 flex items-center gap-3 text-white select-none"> @if ($type === 'server') -
first()->isTerminalEnabled() && $servers->first()->isFunctional()) x-on:terminal-websocket-ready.window="if (!autoConnected) { autoConnected = true; @@ -110,31 +123,25 @@ }" @endif> - + {{ $servers->first()->name }} -
@else -
- +
@if ($containers->count() === 1) - +
+ + {{ data_get($containers->first(), 'container.Names') }} · {{ data_get($containers->first(), 'server.name') }} +
@else -
+
+ class="terminal-target-picker terminal-target-list absolute top-11 left-0 z-50 max-h-72 w-80 overflow-y-auto rounded-lg border p-1 shadow-[0_18px_50px_rgba(0,0,0,0.35)]">