diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php index 830a4eace8..e52d7c32ec 100644 --- a/app/Livewire/Project/Application/Heading.php +++ b/app/Livewire/Project/Application/Heading.php @@ -5,6 +5,7 @@ namespace App\Livewire\Project\Application; use App\Actions\Application\StopApplication; use App\Actions\Docker\GetContainersStatus; use App\Models\Application; +use App\Models\ApplicationDeploymentQueue; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; @@ -79,6 +80,19 @@ class Heading extends Component $this->checkStatus(); } + /** + * Log-page URL of the deployment currently running for this application, so a + * "Deploying… View log" indicator can link back to it after the user navigates + * away. Re-evaluated on the heading's 10s poll. Null when nothing is running. + */ + public function getRunningDeploymentUrlProperty(): ?string + { + return ApplicationDeploymentQueue::where('application_id', $this->application->id) + ->whereIn('status', ['in_progress', 'queued']) + ->orderByDesc('id') + ->value('deployment_url'); + } + public function force_deploy_without_cache() { try { diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php index 993200b578..9a7a8634aa 100644 --- a/app/Livewire/Project/Database/Heading.php +++ b/app/Livewire/Project/Database/Heading.php @@ -6,9 +6,11 @@ use App\Actions\Database\RestartDatabase; use App\Actions\Database\StartDatabase; use App\Actions\Database\StopDatabase; use App\Actions\Docker\GetContainersStatus; +use App\Enums\ProcessStatus; use App\Events\ServiceStatusChanged; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; +use Spatie\Activitylog\Models\Activity; class Heading extends Component { @@ -20,6 +22,10 @@ class Heading extends Component public $docker_cleanup = true; + public $isDeploymentProgress = false; + + public $runningActivityId = null; + public function getListeners() { $teamId = auth()->user()->currentTeam()->id; @@ -61,6 +67,8 @@ class Heading extends Component public function checkStatus() { + $this->checkDeployments(); + if ($this->database->destination->server->isFunctional()) { GetContainersStatus::dispatch($this->database->destination->server); } else { @@ -68,6 +76,52 @@ class Heading extends Component } } + public function checkDeployments() + { + try { + $activity = Activity::where('properties->type_uuid', $this->database->uuid)->latest()->first(); + $status = data_get($activity, 'properties.status'); + if ($status === ProcessStatus::QUEUED->value || $status === ProcessStatus::IN_PROGRESS->value) { + $this->isDeploymentProgress = true; + $this->runningActivityId = $activity->id; + } else { + $this->isDeploymentProgress = false; + $this->runningActivityId = null; + } + } catch (\Throwable) { + $this->isDeploymentProgress = false; + $this->runningActivityId = null; + } + + return $this->isDeploymentProgress; + } + + /** + * Re-attach the live log dialog to a start/restart that is already running, + * so the log reappears after the dialog was closed. + */ + public function reopenDeployment() + { + $this->authorize('view', $this->database); + + $this->checkDeployments(); + + if ($this->isDeploymentProgress && $this->runningActivityId) { + $this->dispatch('activityMonitor', $this->runningActivityId, ServiceStatusChanged::class); + $this->js("window.dispatchEvent(new CustomEvent('startdatabase'))"); + } else { + $this->dispatch('info', 'No operation is currently running.'); + } + } + + private function markDeploymentRunning($activity): void + { + if (is_object($activity)) { + $this->isDeploymentProgress = true; + $this->runningActivityId = $activity->id; + } + } + public function manualCheckStatus() { $this->checkStatus(); @@ -80,6 +134,8 @@ class Heading extends Component 'environment_uuid' => $this->database->environment->uuid, 'database_uuid' => $this->database->uuid, ]; + + $this->checkDeployments(); } public function stop() @@ -102,6 +158,7 @@ class Heading extends Component $activity = RestartDatabase::run($this->database); $this->auditDatabaseAction('ui.database.restarted'); + $this->markDeploymentRunning($activity); $this->js("window.dispatchEvent(new CustomEvent('startdatabase'))"); $this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class); } catch (\Throwable $e) { @@ -116,6 +173,7 @@ class Heading extends Component $activity = StartDatabase::run($this->database); $this->auditDatabaseAction('ui.database.started'); + $this->markDeploymentRunning($activity); $this->js("window.dispatchEvent(new CustomEvent('startdatabase'))"); $this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class); } catch (\Throwable $e) { diff --git a/app/Livewire/Project/Index.php b/app/Livewire/Project/Index.php index f43b7542e6..b1f00b147d 100644 --- a/app/Livewire/Project/Index.php +++ b/app/Livewire/Project/Index.php @@ -57,6 +57,7 @@ class Index extends Component 'href' => $project->navigateTo(), 'environmentCount' => $project->environments->count(), 'resourceCount' => $resourceCount, + 'createdAt' => $project->created_at?->format('M j, Y') ?? '-', 'settingsHref' => auth()->user()->can('update', $project) ? route('project.edit', ['project_uuid' => $project->uuid]) : null, diff --git a/app/Livewire/Project/Service/Heading.php b/app/Livewire/Project/Service/Heading.php index d692a71546..33fce9f079 100644 --- a/app/Livewire/Project/Service/Heading.php +++ b/app/Livewire/Project/Service/Heading.php @@ -27,6 +27,8 @@ class Heading extends Component public $isDeploymentProgress = false; + public $runningActivityId = null; + public $docker_cleanup = true; public $title = 'Configuration'; @@ -35,6 +37,8 @@ class Heading extends Component { $this->authorizeService('view'); + $this->checkDeployments(); + if (str($this->service->status)->contains('running') && is_null($this->service->config_hash)) { $this->service->isConfigurationChanged(true); $this->dispatch('configurationChanged'); @@ -57,6 +61,8 @@ class Heading extends Component { $this->authorizeService('view'); + $this->checkDeployments(); + if ($this->service->server->isFunctional()) { GetContainersStatus::dispatch($this->service->server); } else { @@ -101,22 +107,46 @@ class Heading extends Component $status = data_get($activity, 'properties.status'); if ($status === ProcessStatus::QUEUED->value || $status === ProcessStatus::IN_PROGRESS->value) { $this->isDeploymentProgress = true; + $this->runningActivityId = $activity->id; } else { $this->isDeploymentProgress = false; + $this->runningActivityId = null; } } catch (\Throwable) { $this->isDeploymentProgress = false; + $this->runningActivityId = null; } return $this->isDeploymentProgress; } + /** + * Re-attach the live log dialog to a deployment that is already running. + * Used by the "Deploying…" indicator and when Deploy/Restart is clicked + * while a deployment is in progress, so the running log reappears instead + * of a dead-end error. + */ + public function reopenDeployment() + { + $this->authorizeService('view'); + + $this->checkDeployments(); + + if ($this->isDeploymentProgress && $this->runningActivityId) { + $this->dispatch('activityMonitor', $this->runningActivityId); + $this->js("window.dispatchEvent(new CustomEvent('startservice'))"); + } else { + $this->dispatch('info', 'No deployment is currently running.'); + } + } + public function start() { try { $this->authorizeService('deploy'); $activity = StartService::run($this->service, pullLatestImages: true); $this->auditServiceAction('ui.service.started'); + $this->markDeploymentRunning($activity->id); $this->js("window.dispatchEvent(new CustomEvent('startservice'))"); $this->dispatch('activityMonitor', $activity->id); } catch (\Throwable $e) { @@ -138,6 +168,7 @@ class Heading extends Component $activity->save(); } $activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true); + $this->markDeploymentRunning($activity->id); $this->js("window.dispatchEvent(new CustomEvent('startservice'))"); $this->dispatch('activityMonitor', $activity->id); } catch (\Throwable $e) { @@ -145,6 +176,12 @@ class Heading extends Component } } + private function markDeploymentRunning($activityId): void + { + $this->isDeploymentProgress = true; + $this->runningActivityId = $activityId; + } + public function stop() { try { @@ -168,6 +205,7 @@ class Heading extends Component } $activity = StartService::run($this->service, stopBeforeStart: true); $this->auditServiceAction('ui.service.restarted'); + $this->markDeploymentRunning($activity->id); $this->js("window.dispatchEvent(new CustomEvent('startservice'))"); $this->dispatch('activityMonitor', $activity->id); } catch (\Throwable $e) { @@ -210,6 +248,7 @@ class Heading extends Component } $activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true); $this->auditServiceAction('ui.service.restarted'); + $this->markDeploymentRunning($activity->id); $this->js("window.dispatchEvent(new CustomEvent('startservice'))"); $this->dispatch('activityMonitor', $activity->id); } catch (\Throwable $e) { diff --git a/app/View/Components/Forms/Input.php b/app/View/Components/Forms/Input.php index a1b01e2808..e94b181375 100644 --- a/app/View/Components/Forms/Input.php +++ b/app/View/Components/Forms/Input.php @@ -25,6 +25,7 @@ class Input extends Component public bool $readonly = false, public ?string $helper = null, public bool $allowToPeak = true, + public bool $copyable = false, public bool $isMultiline = false, public string $defaultClass = 'input', public string $autocomplete = 'off', @@ -72,9 +73,15 @@ class Input extends Component } // Durable class (not type-attr based): Alpine may toggle type to "text" when revealing, // and settings-workspace CSS otherwise overrides utility padding-right. - if ($this->type === 'password' && $this->allowToPeak) { + $hasPeek = $this->type === 'password' && $this->allowToPeak; + if ($hasPeek) { $this->defaultClass = $this->defaultClass.' input-with-password-toggle'; } + if ($this->copyable) { + // Reserve clearance for a single copy button, or for both the peek eye + // and the copy button when the field is a maskable password. + $this->defaultClass = $this->defaultClass.($hasPeek ? ' input-with-copy-and-peek' : ' input-with-copy-button'); + } // $this->label = Str::title($this->label); return view('components.forms.input'); diff --git a/resources/css/app.css b/resources/css/app.css index 0e724825f5..b1a8461f3b 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -522,6 +522,11 @@ tr td:first-child { padding-right: 2.5rem; } +/* Room for both the peek eye and the copy button on maskable read-only fields. */ +.input.input-with-copy-and-peek { + padding-right: 4.25rem; +} + .lds-heart { animation: lds-heart 1.2s infinite cubic-bezier(0.215, 0.61, 0.355, 1); } @@ -1958,6 +1963,11 @@ html[data-theme="custom"] textarea:disabled { padding-right: 2.5rem; } +.application-settings-workspace .input.input-with-copy-and-peek, +.application-settings-form .input.input-with-copy-and-peek { + padding-right: 4.25rem; +} + .application-settings-workspace .input:focus-visible, .application-settings-workspace .select:focus-visible, .application-settings-form .input:focus-visible, @@ -4340,11 +4350,11 @@ html[data-theme="custom"] .runtime-log-columns { .projects-table-grid { display: grid; grid-template-columns: - minmax(220px, 1.7fr) - minmax(100px, 0.65fr) - minmax(90px, 0.6fr) - minmax(220px, 1.5fr) - 6rem; + minmax(200px, 1.7fr) + 8rem + 7rem + minmax(200px, 1.6fr) + 5rem; column-gap: 1rem; } @@ -4391,7 +4401,7 @@ html[data-theme="custom"] .runtime-log-columns { @media (max-width: 1050px) { .projects-table-grid { - grid-template-columns: minmax(220px, 1fr) 7rem 6rem 6rem; + grid-template-columns: minmax(200px, 1fr) 8rem 7rem 5rem; } .projects-table-grid .project-description { diff --git a/resources/css/utilities.css b/resources/css/utilities.css index 67d898da3a..96511bbf79 100644 --- a/resources/css/utilities.css +++ b/resources/css/utilities.css @@ -249,6 +249,11 @@ @apply px-2.5 pt-1 pb-1 text-[11px] font-medium text-nav-muted select-none; } +/* Collapsible group header (accordion) for the resource settings sidebar. */ +@utility nav-section-toggle { + @apply w-full items-center justify-between gap-2 px-2.5 pt-1 pb-1 text-[11px] font-medium text-nav-muted select-none rounded-md transition-colors cursor-pointer hover:text-nav-active; +} + /* Indented child rows in a collapsible nav group */ @utility menu-subitem { /* Label owns text ellipsis; keep this row overflow-visible so the focus ring is not clipped. */ @@ -419,15 +424,15 @@ } @media (min-width: 1024px) { - .sidebar-collapsed .menu-item { - justify-content: center; - width: var(--button-h, 2rem); - height: var(--button-h, 2rem); - min-height: var(--button-h, 2rem); - padding-left: 0; - padding-right: 0; - gap: 0; - margin-inline: auto; + /* Collapsed rail keeps every nav icon at its expanded x-position (left-aligned, + same 10px inset) so muscle memory holds when the sidebar is toggled; only the + label is hidden. Unlayered rule outranks the inline lg:justify-center/lg:px-0 + utilities on each row. The footer collapse toggle keeps its own centered + square via the .sidebar-toggle exclusion. */ + .sidebar-collapsed .menu-item:not(.sidebar-toggle) { + justify-content: flex-start; + padding-left: 0.625rem; + padding-right: 0.625rem; } .sidebar-collapsed .sidebar-collapsed-label { diff --git a/resources/js/app.js b/resources/js/app.js index 2e0056011b..b912d827e6 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,4 +1,5 @@ import { initializeCopyButtonComponent } from './copy-button.js'; +import { initializeSettingsSidebarAccordionComponent } from './settings-sidebar-accordion.js'; import { initializeTerminalComponent } from './terminal.js'; import './traffic-globe.js'; import { registerLivewireRequestFailureHandler } from './livewire-request-failure.js'; @@ -20,6 +21,7 @@ document.addEventListener('livewire:navigated', () => { // available before Alpine processes terminal markup after wire:navigate. document.addEventListener('alpine:init', initializeTerminalComponent); document.addEventListener('alpine:init', initializeCopyButtonComponent); +document.addEventListener('alpine:init', initializeSettingsSidebarAccordionComponent); /** * Smooth-scroll a settings section into view, then flash its border for 500ms diff --git a/resources/js/settings-sidebar-accordion.js b/resources/js/settings-sidebar-accordion.js new file mode 100644 index 0000000000..fc62865f0b --- /dev/null +++ b/resources/js/settings-sidebar-accordion.js @@ -0,0 +1,57 @@ +// Alpine data provider for the collapsible resource settings sidebar +// (x-data="settingsSidebarAccordion({ activeGroup, storageKey })"). +// +// Only the group that contains the current page is open by default; every group +// can be collapsed/expanded and the choice is remembered per resource type. The +// active group is always forced open on load so the current page stays reachable. +export function initializeSettingsSidebarAccordionComponent() { + window.Alpine.data('settingsSidebarAccordion', (config = {}) => ({ + activeGroup: config.activeGroup || '', + storageKey: config.storageKey || 'coolify.settings-sidebar', + groups: {}, + // Optional client-side filter (sidebars that render a search box). + search: '', + labels: Array.isArray(config.labels) ? config.labels : [], + get searching() { + return this.search.trim() !== ''; + }, + matches(label) { + if (!this.searching) { + return true; + } + return String(label).toLowerCase().includes(this.search.trim().toLowerCase()); + }, + get hasResults() { + return !this.searching || this.labels.some((label) => this.matches(label)); + }, + init() { + let stored = {}; + try { + stored = JSON.parse(localStorage.getItem(this.storageKey)) || {}; + } catch (e) { + stored = {}; + } + this.groups = stored && typeof stored === 'object' ? stored : {}; + }, + isOpen(group) { + // The current page must stay visible, even when this group was + // previously stored as collapsed on another page. + if (group === this.activeGroup) { + return true; + } + + if (Object.prototype.hasOwnProperty.call(this.groups, group)) { + return this.groups[group]; + } + return false; + }, + toggle(group) { + this.groups = { ...this.groups, [group]: !this.isOpen(group) }; + try { + localStorage.setItem(this.storageKey, JSON.stringify(this.groups)); + } catch (e) { + // ignore storage errors (private mode, quota, etc.) + } + }, + })); +} diff --git a/resources/views/components/application/configuration-sidebar.blade.php b/resources/views/components/application/configuration-sidebar.blade.php index 545e2f4c21..4a64a347ca 100644 --- a/resources/views/components/application/configuration-sidebar.blade.php +++ b/resources/views/components/application/configuration-sidebar.blade.php @@ -179,6 +179,9 @@ ->values()) ->filter(fn ($items) => $items->isNotEmpty()); + // Group that holds the current page — the only one expanded by default. + $activeGroup = (string) $groupedMenuItems->search(fn ($items) => $items->contains(fn ($item) => $item['active'] ?? false)); + // In-page sections (cards) shown as sub-items under the active page $isComposeApp = $application->build_pack === 'dockercompose'; $pageSections = [ @@ -239,6 +242,34 @@ ['id' => 'move-resource-section', 'label' => 'Move resource'], ], ]; + + // Flat, searchable index: every page plus its in-page sub-sections. Each + // entry carries a breadcrumb (its category, and parent page for a + // sub-section) and combined text so the query matches sub-pages too. + $searchIndex = []; + foreach ($groupedMenuItems as $groupLabel => $groupItems) { + foreach ($groupItems as $item) { + $searchIndex[] = [ + 'label' => $item['label'], + 'breadcrumb' => $groupLabel, + 'searchText' => $item['label'].' '.$groupLabel, + 'href' => route($item['route'], $applicationRouteParameters), + 'icon' => $menuIcons[$item['label']] ?? 'settings', + 'navigate' => $item['navigate'] ?? true, + ]; + foreach ($pageSections[$item['route']] ?? [] as $section) { + $searchIndex[] = [ + 'label' => $section['label'], + 'breadcrumb' => $groupLabel.' · '.$item['label'], + 'searchText' => $section['label'].' '.$item['label'].' '.$groupLabel, + 'href' => route($item['route'], $applicationRouteParameters).'#'.$section['id'], + 'icon' => $menuIcons[$item['label']] ?? 'settings', + 'navigate' => true, + ]; + } + } + } + $searchTexts = array_column($searchIndex, 'searchText'); @endphp diff --git a/resources/views/components/database-status-info.blade.php b/resources/views/components/database-status-info.blade.php index b9298e2689..1ff2473f78 100644 --- a/resources/views/components/database-status-info.blade.php +++ b/resources/views/components/database-status-info.blade.php @@ -23,10 +23,10 @@ @else - @if ($dbUrlPublic) - @elseif ($showPublicUrlPlaceholder) values()) ->filter(fn ($items) => $items->isNotEmpty()); + // Group that holds the current page — the only one expanded by default. + $activeGroup = (string) $groupedItems->search(fn ($items) => $items->contains(fn ($item) => $item['active'] ?? false)); + $pageSections = $database->type() === 'standalone-postgresql' ? [ ['id' => 'database-details-section', 'label' => 'Database details'], @@ -62,12 +65,22 @@ diff --git a/resources/views/components/deploying-indicator.blade.php b/resources/views/components/deploying-indicator.blade.php new file mode 100644 index 0000000000..912af3537a --- /dev/null +++ b/resources/views/components/deploying-indicator.blade.php @@ -0,0 +1,34 @@ +@props([ + 'action' => 'reopenDeployment', + 'label' => 'Deploying', + 'href' => null, +]) + +@php + // Persistent affordance shown while a deploy/start is running. Either re-opens + // the in-page live-log dialog (services/databases, via $wire) or links to the + // running deployment's log page (applications, via href) so the log is never lost. + $deployingIndicatorClasses = 'inline-flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1 text-[11px] font-medium ring-1 transition-colors bg-coollabs/10 text-coollabs ring-coollabs/25 hover:bg-coollabs/15 hover:no-underline dark:bg-warning/15 dark:text-warning dark:ring-warning/25 dark:hover:bg-warning/20'; +@endphp + +@if ($href) + class($deployingIndicatorClasses) }} + title="View the running deployment log"> + + {{ $label }}… + View log + +@else + +@endif diff --git a/resources/views/components/forms/input.blade.php b/resources/views/components/forms/input.blade.php index 4a94d3e02e..69cb937879 100644 --- a/resources/views/components/forms/input.blade.php +++ b/resources/views/components/forms/input.blade.php @@ -1,3 +1,18 @@ +@php + // Copy affordance reads the live Livewire value. The bound property comes + // either from the `id`-derived modelBinding or from a passthrough + // `wire:model` attribute (used by read-only fields like DB URLs). Resolve to a + // single JS expression here — a directive inside the tag would + // break Blade's component-tag compiler. + $copyResolve = null; + if ($copyable) { + $copyModel = $modelBinding !== 'null' ? $modelBinding : $attributes->get('wire:model'); + $copyResolve = $copyModel + ? "\$wire.get('".$copyModel."')" + : (string) \Illuminate\Support\Js::from($value); + } +@endphp +
$isMultiline, 'w-full' => !$isMultiline, @@ -48,9 +63,16 @@ @endif + @if ($copyable) + + @endif
@else + @if ($copyable) +
+ @endif merge(['class' => $defaultClass]) }} @required($required) @readonly($readonly) @if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="[box-shadow:inset_4px_0_0_#6b16ed,inset_0_0_0_2px_#e5e5e5] dark:[box-shadow:inset_4px_0_0_#fcd452,inset_0_0_0_2px_#242424]" @endif @@ -61,6 +83,11 @@ @if ($htmlId !== 'null') id={{ $htmlId }} @endif name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}" @if ($autofocus) x-ref="autofocusInput" autofocus @endif> + @if ($copyable) + +
+ @endif @endif @if (!$label && $helper) diff --git a/resources/views/components/navbar.blade.php b/resources/views/components/navbar.blade.php index 251628c496..6975eb1e0e 100644 --- a/resources/views/components/navbar.blade.php +++ b/resources/views/components/navbar.blade.php @@ -52,11 +52,10 @@ }"> {{-- Search is only useful when workspace resources are available --}} @if (isSubscribed() || ! isCloud()) -
+
+ @endforeach diff --git a/resources/views/components/service/configuration-sidebar.blade.php b/resources/views/components/service/configuration-sidebar.blade.php index 5e58b99b08..6ad001c0c6 100644 --- a/resources/views/components/service/configuration-sidebar.blade.php +++ b/resources/views/components/service/configuration-sidebar.blade.php @@ -46,27 +46,41 @@ ->filter() ->values()) ->filter(fn ($items) => $items->isNotEmpty()); + + // Group that holds the current page — the only one expanded by default. + $activeGroup = (string) $groupedItems->search(fn ($items) => $items->contains(fn ($item) => $item['active'] ?? false)); @endphp diff --git a/resources/views/components/shared-variables/layout.blade.php b/resources/views/components/shared-variables/layout.blade.php index 4fd2b58697..c27d72fa52 100644 --- a/resources/views/components/shared-variables/layout.blade.php +++ b/resources/views/components/shared-variables/layout.blade.php @@ -8,14 +8,14 @@ ]; @endphp -
+

Shared variables

Reusable environment variables across resources

-
-
-
-

- {{ $project->environments->count() }} - {{ str('env')->plural($project->environments->count()) }} - · - {{ $resourceCount }} {{ str('resource')->plural($resourceCount) }} -

+
+
+ + + {{ $project->environments->count() }} + + + + {{ $resourceCount }} + +
@if ($firstEnvironment) diff --git a/resources/views/livewire/dashboard/server-metrics-chart.blade.php b/resources/views/livewire/dashboard/server-metrics-chart.blade.php index 7d6924d46c..afb1820e07 100644 --- a/resources/views/livewire/dashboard/server-metrics-chart.blade.php +++ b/resources/views/livewire/dashboard/server-metrics-chart.blade.php @@ -102,9 +102,11 @@ timeZoneName: 'short', }); + const dot = color => ``; + return `
-
CPU: ${formatPercent(cpu)}
-
Memory: ${formatPercent(memory)}
+
${dot('rgb(30, 144, 255)')}CPU: ${formatPercent(cpu)}
+
${dot('rgb(168, 85, 247)')}Memory: ${formatPercent(memory)}
Your time: ${formatLocalTimestamp(timestamp)}
UTC: ${formatUtcTimestamp(timestamp)}
`; diff --git a/resources/views/livewire/notifications/discord.blade.php b/resources/views/livewire/notifications/discord.blade.php index 9e568c8459..9e3921f514 100644 --- a/resources/views/livewire/notifications/discord.blade.php +++ b/resources/views/livewire/notifications/discord.blade.php @@ -11,7 +11,8 @@ description="Send team notifications to a Discord channel through an incoming webhook."> + toggleMethod="instantSaveDiscordEnabled" :canUpdate="auth()->user()->can('update', $settings)" + :canResource="$settings" />
diff --git a/resources/views/livewire/notifications/pushover.blade.php b/resources/views/livewire/notifications/pushover.blade.php index d617ed76cf..40661fc976 100644 --- a/resources/views/livewire/notifications/pushover.blade.php +++ b/resources/views/livewire/notifications/pushover.blade.php @@ -11,7 +11,8 @@ description="Deliver team alerts through your Pushover application."> + toggleMethod="instantSavePushoverEnabled" :canUpdate="auth()->user()->can('update', $settings)" + :canResource="$settings" />
diff --git a/resources/views/livewire/notifications/slack.blade.php b/resources/views/livewire/notifications/slack.blade.php index 9b1814b69d..f3ccd8256d 100644 --- a/resources/views/livewire/notifications/slack.blade.php +++ b/resources/views/livewire/notifications/slack.blade.php @@ -11,7 +11,8 @@ description="Send team notifications to Slack through an incoming webhook."> + toggleMethod="instantSaveSlackEnabled" :canUpdate="auth()->user()->can('update', $settings)" + :canResource="$settings" />
diff --git a/resources/views/livewire/notifications/telegram.blade.php b/resources/views/livewire/notifications/telegram.blade.php index 8686d40a3b..8ab110e62e 100644 --- a/resources/views/livewire/notifications/telegram.blade.php +++ b/resources/views/livewire/notifications/telegram.blade.php @@ -11,7 +11,8 @@ description="Deliver team notifications through a Telegram bot and chat."> + toggleMethod="instantSaveTelegramEnabled" :canUpdate="auth()->user()->can('update', $settings)" + :canResource="$settings" />
diff --git a/resources/views/livewire/notifications/webhook.blade.php b/resources/views/livewire/notifications/webhook.blade.php index 81eb8c2025..fde9054b8d 100644 --- a/resources/views/livewire/notifications/webhook.blade.php +++ b/resources/views/livewire/notifications/webhook.blade.php @@ -11,7 +11,8 @@ description="Send JSON event payloads to your own HTTP endpoint."> + toggleMethod="instantSaveWebhookEnabled" :canUpdate="auth()->user()->can('update', $settings)" + :canResource="$settings" />
diff --git a/resources/views/livewire/project/application/heading.blade.php b/resources/views/livewire/project/application/heading.blade.php index e2af1e96aa..fd41fc99ac 100644 --- a/resources/views/livewire/project/application/heading.blade.php +++ b/resources/views/livewire/project/application/heading.blade.php @@ -33,6 +33,9 @@
+ @if ($this->runningDeploymentUrl) + + @endif
@@ -140,6 +143,9 @@
+ @if ($this->runningDeploymentUrl) + + @endif @if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw)) Load a Compose file to deploy. @else diff --git a/resources/views/livewire/project/database/heading.blade.php b/resources/views/livewire/project/database/heading.blade.php index 2d28510165..92c33bfb8b 100644 --- a/resources/views/livewire/project/database/heading.blade.php +++ b/resources/views/livewire/project/database/heading.blade.php @@ -57,7 +57,8 @@ -
+

@@ -65,6 +66,9 @@

+ @if ($isDeploymentProgress) + + @endif
@@ -77,9 +81,11 @@ @can('manage', $database) @if (! $databaseStatus->startsWith('exited')) - - - Restart + + + + Restart @else - - - Start + + + + Start @endif @@ -103,13 +110,18 @@
+ @if ($isDeploymentProgress) + + @endif @if ($database->destination->server->isFunctional()) @can('manage', $database) @if (! $databaseStatus->startsWith('exited')) - - - Restart + + + + Restart @else - - - Start + + + + Start @endif @@ -162,14 +175,34 @@ @script @endscript diff --git a/resources/views/livewire/project/database/scheduled-backups.blade.php b/resources/views/livewire/project/database/scheduled-backups.blade.php index e52586eaaf..793cd2ff47 100644 --- a/resources/views/livewire/project/database/scheduled-backups.blade.php +++ b/resources/views/livewire/project/database/scheduled-backups.blade.php @@ -100,13 +100,13 @@ {{ $backup->save_s3 ? ($backup->s3?->name ?? 'Unavailable') : 'Local only' }}
@endforeach diff --git a/resources/views/livewire/project/index.blade.php b/resources/views/livewire/project/index.blade.php index 108b4024ac..0a12eac63c 100644 --- a/resources/views/livewire/project/index.blade.php +++ b/resources/views/livewire/project/index.blade.php @@ -125,14 +125,19 @@
-
-

- - · - -

+ -
-
+
+ + + + + + + + +
+

diff --git a/resources/views/livewire/project/resource/index.blade.php b/resources/views/livewire/project/resource/index.blade.php index 8b422ac55e..f270e45217 100644 --- a/resources/views/livewire/project/resource/index.blade.php +++ b/resources/views/livewire/project/resource/index.blade.php @@ -12,6 +12,14 @@

+ + + Shared variables + @can('update', $project) values()) ->filter(fn ($items) => $items->isNotEmpty()); + // Group that holds the current page — the only one expanded by default. + $activeGroup = (string) $groupedItems->search(fn ($items) => $items->contains(fn ($item) => $item['active'] ?? false)); + $storageSections = $applications ->concat($databases) ->map(fn ($resource): array => [ @@ -59,13 +62,23 @@
diff --git a/resources/views/livewire/project/service/heading.blade.php b/resources/views/livewire/project/service/heading.blade.php index 8c6531f3bc..29b9523092 100644 --- a/resources/views/livewire/project/service/heading.blade.php +++ b/resources/views/livewire/project/service/heading.blade.php @@ -65,7 +65,8 @@ -
+

@@ -74,6 +75,9 @@
+ @if ($isDeploymentProgress) + + @endif
@if ($selectedResource) @@ -95,8 +99,9 @@ @elseif ($serviceStatus->contains('running') || $serviceStatus->contains('degraded')) - - Restart + + + Restart @if ($serviceStatus->contains('running'))
+ @php $renderedScope = null; @endphp @foreach ($this->environmentVariablePageRows as $row) + @if ($groupByScope && $row['scope'] !== $renderedScope) + @php $renderedScope = $row['scope']; @endphp +
+ {{ $renderedScope === 'preview' ? 'Preview deployments' : 'Production' }} +
+ @endif @if ($row['kind'] === 'managed') diff --git a/resources/views/livewire/server/index.blade.php b/resources/views/livewire/server/index.blade.php index 52a0c3a990..c86b499396 100644 --- a/resources/views/livewire/server/index.blade.php +++ b/resources/views/livewire/server/index.blade.php @@ -59,6 +59,8 @@ 'href' => route('server.show', ['server_uuid' => $server->uuid]), 'status' => $status, 'statusType' => $statusType, + 'ip' => $server->isLocalhost() ? 'localhost' : ($server->ip ?: '-'), + 'resourceCount' => $server->definedResources()->count(), ]; })->values(); @endphp @@ -172,13 +174,15 @@
+ class="grid min-w-[480px] grid-cols-[minmax(0,1fr)_9.5rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 md:min-w-[640px] md:grid-cols-[minmax(0,1fr)_11rem_6rem_9.5rem] dark:border-white/[0.08] dark:bg-white/[0.05] dark:text-fg-faint">
Server
+ +
Status