mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-17 12:16:11 +00:00
feat(ui): unify resource nav sidebars and polish project layouts
Extract application/database/service configuration sidebars and unified headings; improve clone-to-environment, embedded deployments, storage actions, OAuth icons, terminal, and shared form components.
This commit is contained in:
1 parent
2291c3fa1c
commit
5de6f97256
85 files changed
+2087
-981
No files matched your search
@@ -50,174 +50,184 @@
|
||||
<livewire:project.shared.configuration-checker :resource="$application" />
|
||||
<livewire:project.application.heading :application="$application" wire:key="application-heading-backup-index" />
|
||||
|
||||
<div class="application-settings-form flex flex-col gap-6">
|
||||
<x-application.settings-section title="Storage backups"
|
||||
helper="Schedule backups for persistent volumes and directory mounts attached to this application.">
|
||||
@can('update', $application)
|
||||
<x-slot:actions>
|
||||
<x-modal-input title="New scheduled backup" :wireIgnore="false">
|
||||
<x-slot:content>
|
||||
<button type="button"
|
||||
class="button bg-coollabs/10! text-coollabs! ring-1 ring-coollabs/25 hover:bg-coollabs/15! dark:bg-warning/15! dark:text-warning! dark:ring-warning/25 dark:hover:bg-warning/20!">
|
||||
<x-reicon name="plus" class="size-3.5" />
|
||||
Add
|
||||
</button>
|
||||
</x-slot:content>
|
||||
<livewire:project.application.backup.create :application="$application"
|
||||
wire:key="create-volume-backup-{{ $application->id }}" />
|
||||
</x-modal-input>
|
||||
</x-slot:actions>
|
||||
@endcan
|
||||
<section class="application-settings-workspace mt-4 w-full max-w-[1180px] lg:mt-0">
|
||||
<div class="grid min-w-0 gap-8 xl:grid-cols-[210px_minmax(0,1fr)] xl:gap-10">
|
||||
<x-application.configuration-sidebar :application="$application"
|
||||
current-route="project.application.backup.index" />
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-3">
|
||||
<div>
|
||||
<p class="text-xs font-medium text-neutral-500 dark:text-fg-dim">Schedules</p>
|
||||
<p class="mt-1 text-xl font-semibold tabular-nums text-neutral-950 dark:text-fg">
|
||||
{{ $backups->count() }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-medium text-neutral-500 dark:text-fg-dim">Enabled</p>
|
||||
<p class="mt-1 text-xl font-semibold tabular-nums text-neutral-950 dark:text-fg">
|
||||
{{ $backups->where('enabled', true)->count() }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-medium text-neutral-500 dark:text-fg-dim">Total executions</p>
|
||||
<p class="mt-1 text-xl font-semibold tabular-nums text-neutral-950 dark:text-fg">
|
||||
{{ $backups->sum('executions_count') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</x-application.settings-section>
|
||||
<div class="application-settings-form flex min-w-0 flex-col gap-6">
|
||||
<x-application.settings-section title="Storage backups"
|
||||
helper="Schedule backups for persistent volumes and directory mounts attached to this application.">
|
||||
@can('update', $application)
|
||||
<x-slot:actions>
|
||||
<x-modal-input title="New scheduled backup" :wireIgnore="false">
|
||||
<x-slot:content>
|
||||
<button type="button"
|
||||
class="button bg-coollabs/10! text-coollabs! ring-1 ring-coollabs/25 hover:bg-coollabs/15! dark:bg-warning/15! dark:text-warning! dark:ring-warning/25 dark:hover:bg-warning/20!">
|
||||
<x-reicon name="plus" class="size-3.5" />
|
||||
Add
|
||||
</button>
|
||||
</x-slot:content>
|
||||
<livewire:project.application.backup.create :application="$application"
|
||||
wire:key="create-volume-backup-{{ $application->id }}" />
|
||||
</x-modal-input>
|
||||
</x-slot:actions>
|
||||
@endcan
|
||||
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="relative w-full sm:max-w-sm">
|
||||
<x-reicon name="search"
|
||||
class="pointer-events-none absolute top-1/2 left-2.5 z-10 size-3.5 -translate-y-1/2 text-neutral-400 dark:text-fg-faint" />
|
||||
<input type="search" x-model="search" placeholder="Search backups" aria-label="Search backups"
|
||||
class="input h-8! w-full py-0! pr-8! pl-8!" />
|
||||
<button x-cloak x-show="search" x-on:click="search = ''" type="button"
|
||||
class="absolute top-1/2 right-2 flex size-5 -translate-y-1/2 items-center justify-center rounded text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-faint dark:hover:bg-white/[0.07] dark:hover:text-fg"
|
||||
aria-label="Clear search">
|
||||
<span class="text-sm leading-none">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative" x-on:click.outside="filterOpen = false">
|
||||
<button type="button" class="button" x-on:click="filterOpen = !filterOpen; sortOpen = false">
|
||||
<svg class="size-3.5 opacity-65" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M4 6h16M7 12h10M10 18h4" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" />
|
||||
</svg>
|
||||
Filter
|
||||
</button>
|
||||
<div x-cloak x-show="filterOpen" x-transition.origin.top.right
|
||||
class="absolute top-9 right-0 z-50 min-w-48 rounded-lg border border-neutral-200 bg-white p-1 shadow-modal dark:border-white/[0.1] dark:bg-raised">
|
||||
<template x-for="option in filterOptions" :key="option.value">
|
||||
<button type="button"
|
||||
class="flex h-8 w-full items-center rounded-md px-2 text-left text-[12px] text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
x-on:click="typeFilter = option.value; filterOpen = false">
|
||||
<span class="flex-1" x-text="option.label"></span>
|
||||
<svg x-show="typeFilter === option.value" class="size-3.5 text-warning"
|
||||
viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<path d="m2.5 6.25 2.1 2.1 4.9-5" stroke="currentColor" stroke-width="1.4"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
<div class="grid gap-4 sm:grid-cols-3">
|
||||
<div>
|
||||
<p class="text-xs font-medium text-neutral-500 dark:text-fg-dim">Schedules</p>
|
||||
<p class="mt-1 text-xl font-semibold tabular-nums text-neutral-950 dark:text-fg">
|
||||
{{ $backups->count() }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-medium text-neutral-500 dark:text-fg-dim">Enabled</p>
|
||||
<p class="mt-1 text-xl font-semibold tabular-nums text-neutral-950 dark:text-fg">
|
||||
{{ $backups->where('enabled', true)->count() }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-medium text-neutral-500 dark:text-fg-dim">Total executions</p>
|
||||
<p class="mt-1 text-xl font-semibold tabular-nums text-neutral-950 dark:text-fg">
|
||||
{{ $backups->sum('executions_count') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</x-application.settings-section>
|
||||
|
||||
<div class="relative" x-on:click.outside="sortOpen = false">
|
||||
<button type="button" class="button" x-on:click="sortOpen = !sortOpen; filterOpen = false">
|
||||
<svg class="size-3.5 opacity-65" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M8 5v14m0 0-3-3m3 3 3-3M16 19V5m0 0-3 3m3-3 3 3" stroke="currentColor"
|
||||
stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
Sort
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="relative w-full sm:max-w-sm">
|
||||
<x-reicon name="search"
|
||||
class="pointer-events-none absolute top-1/2 left-2.5 z-10 size-3.5 -translate-y-1/2 text-neutral-400 dark:text-fg-faint" />
|
||||
<input type="search" x-model="search" placeholder="Search backups" aria-label="Search backups"
|
||||
class="input h-8! w-full py-0! pr-8! pl-8!" />
|
||||
<button x-cloak x-show="search" x-on:click="search = ''" type="button"
|
||||
class="absolute top-1/2 right-2 flex size-5 -translate-y-1/2 items-center justify-center rounded text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-faint dark:hover:bg-white/[0.07] dark:hover:text-fg"
|
||||
aria-label="Clear search">
|
||||
<span class="text-sm leading-none">×</span>
|
||||
</button>
|
||||
<div x-cloak x-show="sortOpen" x-transition.origin.top.right
|
||||
class="absolute top-9 right-0 z-50 min-w-48 rounded-lg border border-neutral-200 bg-white p-1 shadow-modal dark:border-white/[0.1] dark:bg-raised">
|
||||
<template x-for="option in sortOptions" :key="option.value">
|
||||
<button type="button"
|
||||
class="flex h-8 w-full items-center rounded-md px-2 text-left text-[12px] text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
x-on:click="sortBy = option.value; sortOpen = false">
|
||||
<span class="flex-1" x-text="option.label"></span>
|
||||
<svg x-show="sortBy === option.value" class="size-3.5 text-warning"
|
||||
viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<path d="m2.5 6.25 2.1 2.1 4.9-5" stroke="currentColor" stroke-width="1.4"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative" x-on:click.outside="filterOpen = false">
|
||||
<button type="button" class="button" x-on:click="filterOpen = !filterOpen; sortOpen = false">
|
||||
<svg class="size-3.5 opacity-65" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M4 6h16M7 12h10M10 18h4" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" />
|
||||
</svg>
|
||||
Filter
|
||||
</button>
|
||||
<div x-cloak x-show="filterOpen" x-transition.origin.top.right
|
||||
class="absolute top-9 right-0 z-50 min-w-48 rounded-lg border border-neutral-200 bg-white p-1 shadow-modal dark:border-white/[0.1] dark:bg-raised">
|
||||
<template x-for="option in filterOptions" :key="option.value">
|
||||
<button type="button"
|
||||
class="flex h-8 w-full items-center rounded-md px-2 text-left text-[12px] text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
x-on:click="typeFilter = option.value; filterOpen = false">
|
||||
<span class="flex-1" x-text="option.label"></span>
|
||||
<svg x-show="typeFilter === option.value" class="size-3.5 text-warning"
|
||||
viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<path d="m2.5 6.25 2.1 2.1 4.9-5" stroke="currentColor" stroke-width="1.4"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative" x-on:click.outside="sortOpen = false">
|
||||
<button type="button" class="button" x-on:click="sortOpen = !sortOpen; filterOpen = false">
|
||||
<svg class="size-3.5 opacity-65" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M8 5v14m0 0-3-3m3 3 3-3M16 19V5m0 0-3 3m3-3 3 3" stroke="currentColor"
|
||||
stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
Sort
|
||||
</button>
|
||||
<div x-cloak x-show="sortOpen" x-transition.origin.top.right
|
||||
class="absolute top-9 right-0 z-50 min-w-48 rounded-lg border border-neutral-200 bg-white p-1 shadow-modal dark:border-white/[0.1] dark:bg-raised">
|
||||
<template x-for="option in sortOptions" :key="option.value">
|
||||
<button type="button"
|
||||
class="flex h-8 w-full items-center rounded-md px-2 text-left text-[12px] text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-dim dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
x-on:click="sortBy = option.value; sortOpen = false">
|
||||
<span class="flex-1" x-text="option.label"></span>
|
||||
<svg x-show="sortBy === option.value" class="size-3.5 text-warning"
|
||||
viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<path d="m2.5 6.25 2.1 2.1 4.9-5" stroke="currentColor" stroke-width="1.4"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div @class([
|
||||
'application-settings-section-body w-full',
|
||||
'is-flush' => $backups->isNotEmpty(),
|
||||
])>
|
||||
<div x-cloak x-show="backups.length > 0 && filteredBackups.length === 0">
|
||||
<x-empty size="sm" title="No backups found"
|
||||
description="No scheduled backups match your search." />
|
||||
</div>
|
||||
|
||||
@if ($backups->isNotEmpty())
|
||||
<div class="data-table flex w-full flex-col" x-show="filteredBackups.length > 0">
|
||||
<div class="data-table-header backup-table-grid">
|
||||
<span>Target</span>
|
||||
<span>Type</span>
|
||||
<span>Schedule</span>
|
||||
<span>Status</span>
|
||||
<span>Last run</span>
|
||||
<span class="text-right">Executions</span>
|
||||
</div>
|
||||
|
||||
@foreach ($backups as $backup)
|
||||
@php
|
||||
$latestExecution = $backup->latestExecution;
|
||||
$status = $latestExecution?->status;
|
||||
$statusLabel = match ($status) {
|
||||
'running' => 'In progress',
|
||||
'success' => 'Success',
|
||||
'failed' => 'Failed',
|
||||
default => $backup->enabled ? 'Waiting' : 'Disabled',
|
||||
};
|
||||
$statusType = match ($status) {
|
||||
'running' => 'warning',
|
||||
'success' => 'success',
|
||||
'failed' => 'error',
|
||||
default => 'neutral',
|
||||
};
|
||||
@endphp
|
||||
<a wire:key="volume-backup-{{ $backup->uuid }}"
|
||||
x-show="isVisible(@js((string) $backup->id))"
|
||||
x-bind:style="{ order: backupOrder(@js((string) $backup->id)) }"
|
||||
href="{{ route('project.application.backup.show', [...$parameters, 'backup_uuid' => $backup->uuid]) }}"
|
||||
{{ wireNavigate() }}
|
||||
class="data-table-row backup-table-grid text-[13px] text-neutral-700 dark:text-fg-dim">
|
||||
<span class="min-w-0 truncate font-medium text-neutral-950 dark:text-fg"
|
||||
title="{{ $backup->targetName() }}">
|
||||
{{ $backup->targetName() }}
|
||||
</span>
|
||||
<span>{{ $backup->targetType() }}</span>
|
||||
<span>{{ $backup->frequency }}</span>
|
||||
<span><x-status-badge :status="$statusLabel" :type="$statusType" /></span>
|
||||
<span>
|
||||
{{ $latestExecution?->finished_at?->diffForHumans() ?? ($status === 'running' ? 'Running now' : 'Never') }}
|
||||
</span>
|
||||
<span class="text-right tabular-nums text-neutral-950 dark:text-fg">
|
||||
{{ $backup->executions_count }}
|
||||
</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<x-empty size="sm" title="No scheduled backups"
|
||||
description="Add a persistent volume or directory backup schedule to protect application data."
|
||||
icon-name="storages" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="application-settings-section-body is-flush w-full">
|
||||
<div x-cloak x-show="backups.length > 0 && filteredBackups.length === 0">
|
||||
<x-empty size="sm" title="No backups found"
|
||||
description="No scheduled backups match your search." />
|
||||
</div>
|
||||
|
||||
@if ($backups->isNotEmpty())
|
||||
<div class="data-table flex w-full flex-col" x-show="filteredBackups.length > 0">
|
||||
<div class="data-table-header backup-table-grid">
|
||||
<span>Target</span>
|
||||
<span>Type</span>
|
||||
<span>Schedule</span>
|
||||
<span>Status</span>
|
||||
<span>Last run</span>
|
||||
<span class="text-right">Executions</span>
|
||||
</div>
|
||||
|
||||
@foreach ($backups as $backup)
|
||||
@php
|
||||
$latestExecution = $backup->latestExecution;
|
||||
$status = $latestExecution?->status;
|
||||
$statusLabel = match ($status) {
|
||||
'running' => 'In progress',
|
||||
'success' => 'Success',
|
||||
'failed' => 'Failed',
|
||||
default => $backup->enabled ? 'Waiting' : 'Disabled',
|
||||
};
|
||||
$statusType = match ($status) {
|
||||
'running' => 'warning',
|
||||
'success' => 'success',
|
||||
'failed' => 'error',
|
||||
default => 'neutral',
|
||||
};
|
||||
@endphp
|
||||
<a wire:key="volume-backup-{{ $backup->uuid }}"
|
||||
x-show="isVisible(@js((string) $backup->id))"
|
||||
x-bind:style="{ order: backupOrder(@js((string) $backup->id)) }"
|
||||
href="{{ route('project.application.backup.show', [...$parameters, 'backup_uuid' => $backup->uuid]) }}"
|
||||
{{ wireNavigate() }}
|
||||
class="data-table-row backup-table-grid text-[13px] text-neutral-700 dark:text-fg-dim">
|
||||
<span class="min-w-0 truncate font-medium text-neutral-950 dark:text-fg"
|
||||
title="{{ $backup->targetName() }}">
|
||||
{{ $backup->targetName() }}
|
||||
</span>
|
||||
<span>{{ $backup->targetType() }}</span>
|
||||
<span>{{ $backup->frequency }}</span>
|
||||
<span><x-status-badge :status="$statusLabel" :type="$statusType" /></span>
|
||||
<span>
|
||||
{{ $latestExecution?->finished_at?->diffForHumans() ?? ($status === 'running' ? 'Running now' : 'Never') }}
|
||||
</span>
|
||||
<span class="text-right tabular-nums text-neutral-950 dark:text-fg">
|
||||
{{ $backup->executions_count }}
|
||||
</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<x-empty size="sm" title="No scheduled backups"
|
||||
description="Add a persistent volume or directory backup schedule to protect application data."
|
||||
icon-name="storages" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -5,260 +5,9 @@
|
||||
<livewire:project.shared.configuration-checker :resource="$application" />
|
||||
<livewire:project.application.heading :application="$application" :wire:key="'application-heading-'.$currentRoute" />
|
||||
|
||||
@php
|
||||
$applicationRouteParameters = [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'application_uuid' => $application->uuid,
|
||||
];
|
||||
|
||||
$configurationMenuItems = [
|
||||
[
|
||||
'label' => 'General',
|
||||
'route' => 'project.application.configuration',
|
||||
'active' => $currentRoute === 'project.application.configuration',
|
||||
],
|
||||
[
|
||||
'label' => 'Domains',
|
||||
'route' => 'project.application.domains',
|
||||
'active' => $currentRoute === 'project.application.domains',
|
||||
],
|
||||
[
|
||||
'label' => 'Advanced',
|
||||
'route' => 'project.application.advanced',
|
||||
'active' => $currentRoute === 'project.application.advanced',
|
||||
],
|
||||
[
|
||||
'label' => 'Swarm',
|
||||
'route' => 'project.application.swarm',
|
||||
'active' => $currentRoute === 'project.application.swarm',
|
||||
'visible' => $application->destination->server->isSwarm(),
|
||||
],
|
||||
[
|
||||
'label' => 'Environment Variables',
|
||||
'route' => 'project.application.environment-variables',
|
||||
'active' => $currentRoute === 'project.application.environment-variables',
|
||||
],
|
||||
[
|
||||
'label' => 'Persistent Storage',
|
||||
'route' => 'project.application.persistent-storage',
|
||||
'active' => $currentRoute === 'project.application.persistent-storage',
|
||||
],
|
||||
[
|
||||
'label' => 'Git Source',
|
||||
'route' => 'project.application.source',
|
||||
'active' => $currentRoute === 'project.application.source',
|
||||
'visible' => $application->git_based(),
|
||||
],
|
||||
[
|
||||
'label' => 'Servers',
|
||||
'route' => 'project.application.servers',
|
||||
'active' => $currentRoute === 'project.application.servers',
|
||||
'badge' => true,
|
||||
],
|
||||
[
|
||||
'label' => 'Scheduled Tasks',
|
||||
'route' => 'project.application.scheduled-tasks.show',
|
||||
'active' => str($currentRoute)->startsWith('project.application.scheduled-tasks'),
|
||||
],
|
||||
[
|
||||
'label' => 'Webhooks',
|
||||
'route' => 'project.application.webhooks',
|
||||
'active' => $currentRoute === 'project.application.webhooks',
|
||||
],
|
||||
[
|
||||
'label' => 'Preview Deployments',
|
||||
'route' => 'project.application.preview-deployments',
|
||||
'active' => $currentRoute === 'project.application.preview-deployments',
|
||||
'visible' => $application->git_based() || $application->build_pack === 'dockerimage',
|
||||
],
|
||||
[
|
||||
'label' => 'Healthcheck',
|
||||
'route' => 'project.application.healthcheck',
|
||||
'active' => $currentRoute === 'project.application.healthcheck',
|
||||
'visible' => $application->build_pack !== 'dockercompose',
|
||||
],
|
||||
[
|
||||
'label' => 'Rollback',
|
||||
'route' => 'project.application.rollback',
|
||||
'active' => $currentRoute === 'project.application.rollback',
|
||||
],
|
||||
[
|
||||
'label' => 'Resource Limits',
|
||||
'route' => 'project.application.resource-limits',
|
||||
'active' => $currentRoute === 'project.application.resource-limits',
|
||||
],
|
||||
[
|
||||
'label' => 'Resource Operations',
|
||||
'route' => 'project.application.resource-operations',
|
||||
'active' => $currentRoute === 'project.application.resource-operations',
|
||||
],
|
||||
[
|
||||
'label' => 'Metrics',
|
||||
'route' => 'project.application.metrics',
|
||||
'active' => $currentRoute === 'project.application.metrics',
|
||||
],
|
||||
[
|
||||
'label' => 'Tags',
|
||||
'route' => 'project.application.tags',
|
||||
'active' => $currentRoute === 'project.application.tags',
|
||||
],
|
||||
[
|
||||
'label' => 'Danger Zone',
|
||||
'route' => 'project.application.danger',
|
||||
'active' => $currentRoute === 'project.application.danger',
|
||||
],
|
||||
];
|
||||
|
||||
$configurationMenuItems = array_values(array_filter(
|
||||
$configurationMenuItems,
|
||||
fn (array $item): bool => $item['visible'] ?? true,
|
||||
));
|
||||
|
||||
// Icons follow the main sidebar's reicon set
|
||||
$menuIcons = [
|
||||
'General' => 'settings',
|
||||
'Domains' => 'globe',
|
||||
'Advanced' => 'grid',
|
||||
'Swarm' => 'destinations',
|
||||
'Environment Variables' => 'variables',
|
||||
'Persistent Storage' => 'storages',
|
||||
'Git Source' => 'sources',
|
||||
'Servers' => 'servers',
|
||||
'Scheduled Tasks' => 'calendar',
|
||||
'Webhooks' => 'notifications',
|
||||
'Preview Deployments' => 'eye',
|
||||
'Healthcheck' => 'feedback',
|
||||
'Rollback' => 'time-back',
|
||||
'Resource Limits' => 'cpu',
|
||||
'Resource Operations' => 'server-update',
|
||||
'Metrics' => 'graph',
|
||||
'Tags' => 'tags',
|
||||
'Danger Zone' => 'shield-alert',
|
||||
];
|
||||
|
||||
// Discord-style groups for the settings sidebar
|
||||
$menuGroups = [
|
||||
'Settings' => ['General', 'Domains', 'Advanced', 'Swarm', 'Environment Variables', 'Persistent Storage'],
|
||||
'Build & deploy' => ['Git Source', 'Servers', 'Preview Deployments', 'Rollback', 'Scheduled Tasks', 'Webhooks', 'Healthcheck'],
|
||||
'Operations' => ['Resource Limits', 'Resource Operations', 'Metrics', 'Tags', 'Danger Zone'],
|
||||
];
|
||||
$groupedMenuItems = collect($menuGroups)
|
||||
->map(fn (array $labels) => collect($configurationMenuItems)->whereIn('label', $labels)->values())
|
||||
->filter(fn ($items) => $items->isNotEmpty());
|
||||
|
||||
// In-page sections (cards) shown as sub-items under the active page
|
||||
$isComposeApp = $application->build_pack === 'dockercompose';
|
||||
$pageSections = [
|
||||
'project.application.configuration' => array_values(array_filter([
|
||||
['id' => 'application-details-section', 'label' => 'Application details'],
|
||||
['id' => 'public-access-section', 'label' => 'Public access'],
|
||||
['id' => 'build-pipeline-section', 'label' => 'Build pipeline'],
|
||||
$isComposeApp ? null : ['id' => 'container-image-section', 'label' => 'Container image'],
|
||||
$isComposeApp ? null : ['id' => 'networking-section', 'label' => 'Networking'],
|
||||
$isComposeApp ? null : ['id' => 'runtime-section', 'label' => 'Runtime'],
|
||||
$isComposeApp ? null : ['id' => 'security-section', 'label' => 'Security'],
|
||||
['id' => 'deployment-lifecycle-section', 'label' => 'Deployment lifecycle'],
|
||||
$isComposeApp ? null : ['id' => 'container-labels-section', 'label' => 'Container labels'],
|
||||
])),
|
||||
'project.application.advanced' => array_values(array_filter([
|
||||
['id' => 'advanced-build-section', 'label' => 'Build'],
|
||||
['id' => 'advanced-container-section', 'label' => 'Container'],
|
||||
$application->git_based() ? ['id' => 'advanced-deployment-section', 'label' => 'Deployment'] : null,
|
||||
$application->git_based() ? ['id' => 'advanced-git-section', 'label' => 'Git'] : null,
|
||||
$isComposeApp ? ['id' => 'advanced-compose-section', 'label' => 'Docker compose'] : null,
|
||||
['id' => 'advanced-proxy-section', 'label' => 'Proxy'],
|
||||
['id' => 'advanced-operations-section', 'label' => 'Operations'],
|
||||
['id' => 'advanced-logs-section', 'label' => 'Logs'],
|
||||
$isComposeApp ? null : ['id' => 'advanced-gpu-section', 'label' => 'GPU'],
|
||||
])),
|
||||
'project.application.webhooks' => [
|
||||
['id' => 'deploy-webhook-section', 'label' => 'Deploy webhook'],
|
||||
['id' => 'manual-git-webhooks-section', 'label' => 'Manual Git webhooks'],
|
||||
],
|
||||
'project.application.preview-deployments' => array_values(array_filter([
|
||||
['id' => 'preview-template-section', 'label' => 'URL template'],
|
||||
$application->is_github_based()
|
||||
? ['id' => 'preview-pull-requests-section', 'label' => 'Pull requests']
|
||||
: null,
|
||||
$application->build_pack === 'dockerimage'
|
||||
? ['id' => 'manual-preview-section', 'label' => 'Manual preview']
|
||||
: null,
|
||||
['id' => 'preview-deployments-section', 'label' => 'Deployments'],
|
||||
])),
|
||||
'project.application.healthcheck' => [
|
||||
['id' => 'healthcheck-configuration-section', 'label' => 'Configuration'],
|
||||
[
|
||||
'id' => $application->health_check_type === 'cmd'
|
||||
? 'healthcheck-command-section'
|
||||
: 'healthcheck-request-section',
|
||||
'label' => $application->health_check_type === 'cmd' ? 'Command' : 'HTTP request',
|
||||
],
|
||||
['id' => 'healthcheck-timing-section', 'label' => 'Timing and retries'],
|
||||
],
|
||||
'project.application.rollback' => [
|
||||
['id' => 'rollback-retention-section', 'label' => 'Image retention'],
|
||||
['id' => 'rollback-images-section', 'label' => 'Available images'],
|
||||
],
|
||||
'project.application.resource-limits' => [
|
||||
['id' => 'cpu-limits-section', 'label' => 'CPU'],
|
||||
['id' => 'memory-limits-section', 'label' => 'Memory'],
|
||||
],
|
||||
'project.application.resource-operations' => [
|
||||
['id' => 'clone-resource-section', 'label' => 'Clone resource'],
|
||||
['id' => 'move-resource-section', 'label' => 'Move resource'],
|
||||
],
|
||||
];
|
||||
@endphp
|
||||
|
||||
<section class="application-settings-workspace mt-4 w-full max-w-[1180px] lg:mt-0">
|
||||
<div class="grid min-w-0 gap-8 xl:grid-cols-[210px_minmax(0,1fr)] xl:gap-10">
|
||||
<aside class="application-settings-navigation min-w-0 xl:self-start">
|
||||
<nav aria-label="Configuration sections"
|
||||
class="grid grid-cols-2 gap-0.5 border-y border-neutral-200 py-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-1 xl:border-y-0 xl:py-0 dark:border-white/[0.06]">
|
||||
@foreach ($groupedMenuItems as $groupLabel => $groupItems)
|
||||
@unless ($loop->first)
|
||||
<div class="hidden xl:block my-2 border-t border-neutral-200 dark:border-white/[0.06]" aria-hidden="true"></div>
|
||||
@endunless
|
||||
<div class="nav-section hidden xl:block">{{ $groupLabel }}</div>
|
||||
@foreach ($groupItems as $menuItem)
|
||||
<a wire:key="application-settings-link-{{ str($menuItem['label'])->slug() }}"
|
||||
@class([
|
||||
'menu-item',
|
||||
'menu-item-active' => $menuItem['active'],
|
||||
])
|
||||
{{ wireNavigate() }}
|
||||
href="{{ route($menuItem['route'], $applicationRouteParameters) }}">
|
||||
<x-reicon :name="$menuIcons[$menuItem['label']] ?? 'settings'" class="menu-item-icon" />
|
||||
<span class="menu-item-label">{{ $menuItem['label'] }}</span>
|
||||
@if ($menuItem['badge'] ?? false)
|
||||
<span class="shrink-0">
|
||||
<livewire:project.application.server-status-badge :application="$application" />
|
||||
</span>
|
||||
@endif
|
||||
</a>
|
||||
@if ($menuItem['active'] && count($pageSections[$menuItem['route']] ?? []) >= 4)
|
||||
<div class="nav-children hidden flex-col gap-0.5 py-1 xl:flex"
|
||||
x-data="{
|
||||
activeSection: '',
|
||||
scrollToSection(id) {
|
||||
this.activeSection = id;
|
||||
window.scrollToSettingsSection?.(id);
|
||||
},
|
||||
}">
|
||||
@foreach ($pageSections[$menuItem['route']] as $section)
|
||||
<button type="button" class="menu-subitem"
|
||||
:class="activeSection === '{{ $section['id'] }}' && 'menu-subitem-active'"
|
||||
@click="scrollToSection('{{ $section['id'] }}')">
|
||||
<span class="menu-item-label text-left">{{ $section['label'] }}</span>
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
@endforeach
|
||||
</nav>
|
||||
</aside>
|
||||
<x-application.configuration-sidebar :application="$application" :current-route="$currentRoute" />
|
||||
|
||||
<div class="min-w-0">
|
||||
@if ($currentRoute === 'project.application.configuration')
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<div>
|
||||
<x-slot:title>{{ data_get_str($application, 'name')->limit(10) }} > Deployments | Coolify</x-slot>
|
||||
<livewire:project.shared.configuration-checker :resource="$application" />
|
||||
<livewire:project.application.heading :application="$application" wire:key="application-heading-deployment-index" />
|
||||
@unless ($embedded)
|
||||
<x-slot:title>{{ data_get_str($application, 'name')->limit(10) }} > Deployments | Coolify</x-slot>
|
||||
<livewire:project.shared.configuration-checker :resource="$application" />
|
||||
<livewire:project.application.heading :application="$application" wire:key="application-heading-deployment-index" />
|
||||
@endunless
|
||||
|
||||
@php
|
||||
$lastPage = max(1, (int) ceil($deployments_count / $defaultTake));
|
||||
@@ -11,7 +13,20 @@
|
||||
$hasActiveQuery = trim($search) !== '' || $hasActiveFilter;
|
||||
@endphp
|
||||
|
||||
<div class="application-settings-form"
|
||||
<section @class([
|
||||
'application-settings-workspace w-full',
|
||||
'mt-4 max-w-[1180px] lg:mt-0' => ! $embedded,
|
||||
])>
|
||||
<div @class([
|
||||
'min-w-0',
|
||||
'grid gap-8 xl:grid-cols-[210px_minmax(0,1fr)] xl:gap-10' => ! $embedded,
|
||||
])>
|
||||
@unless ($embedded)
|
||||
<x-application.configuration-sidebar :application="$application"
|
||||
current-route="project.application.deployment.index" />
|
||||
@endunless
|
||||
|
||||
<div class="application-settings-form min-w-0"
|
||||
@if (!$skip) wire:poll.5000ms="reloadDeployments" @endif>
|
||||
<x-application.settings-section title="Deployment history"
|
||||
helper="Search, filter, and open a deployment to inspect its build logs." flush>
|
||||
@@ -204,7 +219,10 @@
|
||||
<a wire:key="deployment-{{ data_get($deployment, 'deployment_uuid') }}"
|
||||
href="{{ $current_url . '/' . data_get($deployment, 'deployment_uuid') }}"
|
||||
{{ wireNavigate() }}
|
||||
class="data-table-row deployment-table-grid border-b border-neutral-200 text-[13px] text-neutral-600 dark:border-white/[0.08] dark:text-fg-dim">
|
||||
@class([
|
||||
'data-table-row deployment-table-grid border-b border-neutral-200 text-[13px] text-neutral-600 dark:border-white/[0.08] dark:text-fg-dim',
|
||||
'data-table-row-active' => $selectedDeploymentUuid === data_get($deployment, 'deployment_uuid'),
|
||||
])>
|
||||
<span><x-status-badge :status="$statusLabel" :type="$statusType" /></span>
|
||||
<span>{{ $sourceLabel }}</span>
|
||||
<span class="min-w-0">
|
||||
@@ -247,4 +265,6 @@
|
||||
@endif
|
||||
</x-application.settings-section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -1,10 +1,20 @@
|
||||
<div class="flex h-[calc(100dvh-7.5rem)] min-h-[22rem] flex-col overflow-hidden sm:h-[calc(100vh-8rem)] sm:min-h-[32rem] lg:min-h-[42rem]">
|
||||
<div
|
||||
class="flex min-h-[calc(100dvh-7.5rem)] flex-col xl:h-[calc(100dvh-7.5rem)] xl:min-h-0 xl:overflow-hidden">
|
||||
<x-slot:title>
|
||||
{{ data_get_str($application, 'name')->limit(10) }} > Deployment | Coolify
|
||||
</x-slot>
|
||||
<livewire:project.shared.configuration-checker :resource="$application" />
|
||||
<livewire:project.application.heading :application="$application" wire:key="application-heading-deployment-show" />
|
||||
<div x-data="{
|
||||
<section class="application-settings-workspace flex min-h-0 w-full max-w-[1180px] flex-1">
|
||||
<div class="grid min-h-0 min-w-0 flex-1 gap-8 xl:grid-cols-[210px_minmax(0,1fr)] xl:gap-10">
|
||||
<x-application.configuration-sidebar :application="$application" :flush="true"
|
||||
current-route="project.application.deployment.show" />
|
||||
<div class="flex min-h-0 min-w-0 flex-col gap-4 xl:h-full xl:pt-px">
|
||||
<livewire:project.application.deployment.index :embedded="true"
|
||||
:selected-deployment-uuid="$deployment_uuid"
|
||||
wire:key="embedded-deployment-history-{{ $deployment_uuid }}" />
|
||||
|
||||
<div x-data="{
|
||||
fullscreen: @entangle('fullscreen'),
|
||||
alwaysScroll: {{ $isKeepAliveOn ? 'true' : 'false' }},
|
||||
rafId: null,
|
||||
@@ -261,8 +271,8 @@
|
||||
this.deploymentFinishedCleanup = null;
|
||||
}
|
||||
}
|
||||
}" class="flex flex-1 min-h-0 flex-col overflow-hidden">
|
||||
<div id="screen" :class="fullscreen ? 'fullscreen flex flex-col' : 'mt-2 flex flex-1 min-h-0 flex-col overflow-hidden lg:mt-3'">
|
||||
}" class="flex h-[calc(100dvh-8rem)] min-h-[32rem] w-full flex-col overflow-hidden xl:h-auto xl:min-h-0 xl:flex-1">
|
||||
<div id="screen" :class="fullscreen ? 'fullscreen flex flex-col' : 'mt-2 flex flex-1 min-h-0 flex-col overflow-hidden lg:mt-0'">
|
||||
<div @if ($isKeepAliveOn) wire:poll.2000ms="polling" @endif
|
||||
class="logs-viewer flex min-h-0 w-full flex-col overflow-hidden bg-[#0d0d0d] text-neutral-100"
|
||||
:class="fullscreen ? 'h-full' : 'flex-1 rounded-xl border border-neutral-800 shadow-sm'">
|
||||
@@ -456,4 +466,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -9,16 +9,16 @@
|
||||
<x-unsaved-bar action="submit" />
|
||||
{{-- Temporarily hidden: the "Compose parser" dev hint and the "View details"
|
||||
resource-details modal trigger. --}}
|
||||
@if ($buildPack === 'dockercompose')
|
||||
<div class="mb-4 flex flex-wrap items-center justify-end gap-2">
|
||||
<x-forms.button canGate="update" :canResource="$application" wire:target='initLoadingCompose'
|
||||
x-on:click="$wire.dispatch('loadCompose', false)">
|
||||
{{ $application->docker_compose_raw ? 'Reload compose' : 'Load compose' }}
|
||||
</x-forms.button>
|
||||
</div>
|
||||
@endif
|
||||
<div class="application-settings-grid flex flex-col gap-6">
|
||||
<x-application.settings-section id="application-details-section" title="Application details" helper="Name the application and choose the build strategy Coolify should use to deploy it." class="application-details-card">
|
||||
@if ($buildPack === 'dockercompose')
|
||||
<x-slot:actions>
|
||||
<x-forms.button canGate="update" :canResource="$application" wire:target='initLoadingCompose'
|
||||
x-on:click="$wire.dispatch('loadCompose', false)">
|
||||
{{ $application->docker_compose_raw ? 'Reload compose' : 'Load compose' }}
|
||||
</x-forms.button>
|
||||
</x-slot:actions>
|
||||
@endif
|
||||
<div class="grid gap-4">
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" id="name" label="Name" required />
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" id="description" label="Description" />
|
||||
@@ -302,7 +302,7 @@
|
||||
@endif
|
||||
@if ($buildPack === 'dockercompose')
|
||||
<div x-data="{ showRaw: true }">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<h3>Docker Compose</h3>
|
||||
<x-forms.button x-show="{{ $application->settings->is_raw_compose_deployment_enabled ? 'false' : 'true' }}"
|
||||
@click.prevent="showRaw = !showRaw"
|
||||
|
||||
@@ -17,28 +17,6 @@
|
||||
'route' => 'project.application.configuration',
|
||||
'active' => $isSettingsRoute,
|
||||
],
|
||||
[
|
||||
'label' => 'Backups',
|
||||
'route' => 'project.application.backup.index',
|
||||
'active' => $routeIs('project.application.backup.*'),
|
||||
],
|
||||
[
|
||||
'label' => 'Console',
|
||||
'route' => 'project.application.command',
|
||||
'active' => $routeIs('project.application.command'),
|
||||
'navigate' => false,
|
||||
'visible' => ! $application->destination->server->isSwarm() && auth()->user()?->can('canAccessTerminal'),
|
||||
],
|
||||
[
|
||||
'label' => 'Deployment Logs',
|
||||
'route' => 'project.application.deployment.index',
|
||||
'active' => $routeIs(['project.application.deployment.index', 'project.application.deployment.show']),
|
||||
],
|
||||
[
|
||||
'label' => 'Runtime Logs',
|
||||
'route' => 'project.application.logs',
|
||||
'active' => $routeIs('project.application.logs'),
|
||||
],
|
||||
];
|
||||
|
||||
$applicationMenuItems = array_values(array_filter(
|
||||
@@ -56,7 +34,7 @@
|
||||
};
|
||||
@endphp
|
||||
<div>
|
||||
<div class="mb-3 w-full lg:hidden">
|
||||
<div class="mb-3 w-full xl:hidden">
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<h1 class="min-w-0 truncate text-[24px]! leading-7! font-semibold! tracking-tight! text-black dark:text-fg">
|
||||
{{ $application->name }}
|
||||
@@ -65,7 +43,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full md:hidden">
|
||||
<div class="w-full xl:hidden">
|
||||
@if (!($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw)))
|
||||
<div id="application-mobile-actions" class="relative mb-3"
|
||||
x-data="{ open: false }" @click.outside="open = false"
|
||||
@@ -179,25 +157,8 @@
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<div
|
||||
class="flex min-w-0 items-center gap-0.5 rounded-[10px] border border-neutral-200 bg-neutral-100 p-1 dark:border-white/[0.07] dark:bg-white/[0.035]">
|
||||
{{-- Tabs may scroll; keep Links outside overflow so the dropdown never creates a scrollbar. --}}
|
||||
<x-resource-heading-tabs class="min-w-0 flex-1">
|
||||
@foreach ($applicationMenuItems as $menuItem)
|
||||
<a @class([
|
||||
'app-tab shrink-0',
|
||||
'app-tab-active' => $menuItem['active'],
|
||||
])
|
||||
@if ($menuItem['active']) aria-current="page" @endif
|
||||
@if ($menuItem['navigate'] ?? true) {{ wireNavigate() }} @endif
|
||||
href="{{ route($menuItem['route'], $parameters) }}">
|
||||
{{ $menuItem['label'] }}
|
||||
</a>
|
||||
@endforeach
|
||||
</x-resource-heading-tabs>
|
||||
<div class="resource-heading-menus shrink-0">
|
||||
<x-applications.links :application="$application" />
|
||||
</div>
|
||||
<div class="resource-heading-menus w-full">
|
||||
<x-applications.links :application="$application" full-width />
|
||||
</div>
|
||||
<div class="hidden" aria-hidden="true">
|
||||
<x-modal-confirmation title="Confirm Application Stopping?" buttonTitle="Stop"
|
||||
@@ -223,35 +184,11 @@
|
||||
</div>
|
||||
|
||||
{{-- Layer-2 top nav: one unified bar — menus left, actions right --}}
|
||||
<div class="hidden w-full items-center md:flex lg:fixed lg:top-12 lg:right-0 lg:z-30 lg:h-12 lg:w-auto lg:border-b lg:border-neutral-200 lg:bg-white/95 lg:pl-2 lg:pr-4 lg:backdrop-blur lg:transition-[left] lg:duration-200 lg:dark:border-white/[0.06] lg:dark:bg-panel/95"
|
||||
:class="[typeof collapsed !== 'undefined' && collapsed ? 'lg:left-16' : 'lg:left-56']">
|
||||
<div
|
||||
class="hidden w-full items-center xl:fixed xl:top-14 xl:right-4 xl:z-30 xl:flex xl:h-12 xl:w-auto xl:border-0 xl:bg-transparent xl:p-0 xl:backdrop-blur-none xl:dark:bg-transparent">
|
||||
<div
|
||||
class="resource-heading-navbar application-heading-actions flex w-full min-w-0 items-center justify-between gap-2 overflow-visible rounded-[10px] border border-neutral-200 bg-neutral-100 p-1 dark:border-white/[0.07] dark:bg-white/[0.035]">
|
||||
<div class="application-primary-tabs flex min-w-0 items-center gap-0.5">
|
||||
{{-- Tabs alone may scroll; keep Links outside overflow so the dropdown never creates a scrollbar. --}}
|
||||
<x-resource-heading-tabs class="min-w-0">
|
||||
@foreach ($applicationMenuItems as $menuItem)
|
||||
<a wire:key="application-primary-nav-{{ str($menuItem['label'])->slug() }}"
|
||||
@class([
|
||||
'app-tab shrink-0',
|
||||
'app-tab-active' => $menuItem['active'],
|
||||
])
|
||||
@if ($menuItem['active']) aria-current="page" @endif
|
||||
@if ($menuItem['navigate'] ?? true) {{ wireNavigate() }} @endif
|
||||
href="{{ route($menuItem['route'], $parameters) }}">
|
||||
{{ $menuItem['label'] }}
|
||||
@if ($menuItem['label'] === 'Runtime Logs' && $application->restart_count > 0 && (!str($application->status)->startsWith('exited') || $application->stoppedAfterRestartLimit()))
|
||||
<span class="size-1.5 rounded-full bg-warning"
|
||||
title="Container has restarted {{ $application->restart_count }} time{{ $application->restart_count > 1 ? 's' : '' }}"></span>
|
||||
@endif
|
||||
</a>
|
||||
@endforeach
|
||||
</x-resource-heading-tabs>
|
||||
<div class="resource-heading-menus shrink-0">
|
||||
<x-applications.links :application="$application" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-heading-actions flex shrink-0 items-center gap-0.5 border-l border-neutral-200 pl-1 dark:border-white/[0.08]">
|
||||
class="resource-heading-navbar application-heading-actions flex w-full min-w-0 items-center justify-start gap-2 overflow-visible rounded-[10px] border border-neutral-200 bg-neutral-100 p-1 xl:w-auto xl:justify-end dark:border-white/[0.07] dark:bg-white/[0.035]">
|
||||
<div class="resource-heading-actions flex shrink-0 items-center gap-0.5">
|
||||
{{-- Status badge temporarily hidden — will be redesigned later:
|
||||
<x-status.index :resource="$application" :title="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" /> --}}
|
||||
@if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
|
||||
@@ -260,56 +197,90 @@
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<x-applications.advanced :application="$application" />
|
||||
@endif
|
||||
@if (!str($application->status)->startsWith('exited'))
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
<x-forms.button canGate="deploy" :canResource="$application" title="With rolling update if possible" wire:click="deploy">
|
||||
<x-reicon name="refresh" class="size-4 text-orange-500 dark:text-orange-400" />
|
||||
Redeploy
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
@if ($application->destination->server->isSwarm())
|
||||
<x-forms.button canGate="deploy" :canResource="$application" title="Redeploy Swarm Service (rolling update)" wire:click="deploy">
|
||||
<x-reicon name="refresh" class="size-4 text-warning" />
|
||||
Update Service
|
||||
</x-forms.button>
|
||||
@else
|
||||
<x-modal-confirmation title="Confirm Application Restart?" buttonTitle="Restart"
|
||||
submitAction="restart" :actions="[
|
||||
'This application will be restarted without rebuilding.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step2ButtonText="Confirm">
|
||||
<x-slot:content>
|
||||
<x-forms.button canGate="deploy" :canResource="$application" title="Restart without rebuilding">
|
||||
<x-reicon name="restart" class="size-4 text-warning" />
|
||||
Restart
|
||||
</x-forms.button>
|
||||
</x-slot:content>
|
||||
</x-modal-confirmation>
|
||||
@endif
|
||||
@endif
|
||||
<x-modal-confirmation :disabled="!auth()->user()->can('deploy', $application)" :authDisabled="!auth()->user()->can('deploy', $application)" title="Confirm Application Stopping?" buttonTitle="Stop"
|
||||
submitAction="stop" :checkboxes="$checkboxes" :actions="[
|
||||
'This application will be stopped.',
|
||||
'All non-persistent data of this application will be deleted.',
|
||||
]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step1ButtonText="Continue" step2ButtonText="Confirm">
|
||||
<x-slot:button-title>
|
||||
<x-reicon name="stop" class="size-4 text-error" />
|
||||
Stop
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<x-forms.button canGate="deploy" :canResource="$application" wire:click="deploy">
|
||||
<div class="resource-heading-menus shrink-0">
|
||||
<x-applications.links :application="$application" />
|
||||
</div>
|
||||
@if (str($application->status)->startsWith('exited'))
|
||||
<x-forms.button id="application-desktop-deploy" canGate="deploy" :canResource="$application"
|
||||
wire:click="deploy">
|
||||
<x-reicon name="play-circle" class="size-4 text-warning" />
|
||||
Deploy
|
||||
</x-forms.button>
|
||||
@else
|
||||
<div id="application-desktop-actions" class="relative" x-data="{ open: false }"
|
||||
@click.outside="open = false" @keydown.escape.window="open = false">
|
||||
<button type="button" class="button" @click="open = !open" :aria-expanded="open"
|
||||
aria-haspopup="menu">
|
||||
<x-reicon name="play-circle" class="size-3.5 opacity-70" />
|
||||
Actions
|
||||
<x-reicon name="chevron-down" class="size-3 opacity-55" />
|
||||
</button>
|
||||
|
||||
<div x-cloak x-show="open" x-transition.origin.top.right
|
||||
class="listbox-panel top-full! right-0! left-auto! mt-1! w-60! min-w-0!" role="menu">
|
||||
@if (!$application->destination->server->isSwarm())
|
||||
@can('deploy', $application)
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
wire:click="deploy" @click="open = false" role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 text-orange-500 dark:text-orange-400" />
|
||||
Redeploy
|
||||
</button>
|
||||
@else
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled>
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Redeploy
|
||||
</button>
|
||||
@endcan
|
||||
@endif
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
@if ($application->destination->server->isSwarm())
|
||||
@can('deploy', $application)
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
wire:click="deploy" @click="open = false" role="menuitem">
|
||||
<x-reicon name="refresh" class="size-3.5 text-warning" />
|
||||
Update Service
|
||||
</button>
|
||||
@else
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled>
|
||||
<x-reicon name="refresh" class="size-3.5 opacity-70" />
|
||||
Update Service
|
||||
</button>
|
||||
@endcan
|
||||
@else
|
||||
@can('deploy', $application)
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="open = false; document.getElementById('application-mobile-restart-trigger')?.click()"
|
||||
role="menuitem">
|
||||
<x-reicon name="restart" class="size-3.5 text-warning" />
|
||||
Restart
|
||||
</button>
|
||||
@else
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled>
|
||||
<x-reicon name="restart" class="size-3.5 opacity-70" />
|
||||
Restart
|
||||
</button>
|
||||
@endcan
|
||||
@endif
|
||||
@endif
|
||||
@can('deploy', $application)
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!"
|
||||
@click="open = false; document.getElementById('application-mobile-stop-trigger')?.click()"
|
||||
role="menuitem">
|
||||
<x-reicon name="stop" class="size-3.5 text-error" />
|
||||
Stop
|
||||
</button>
|
||||
@else
|
||||
<button type="button" class="listbox-option justify-start! gap-2.5!" disabled>
|
||||
<x-reicon name="stop" class="size-3.5 opacity-70" />
|
||||
Stop
|
||||
</button>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{-- Spacer: in-flow stand-in for the fixed layer-2 nav (h-12). Not visible content — keeps page body clear of the bar. --}}
|
||||
<div class="hidden lg:block lg:h-12" aria-hidden="true"></div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -63,6 +63,11 @@
|
||||
icon-name="sources" />
|
||||
@endforelse
|
||||
</div>
|
||||
<div class="w-full" wire:loading wire:target="load_prs">
|
||||
<div class="flex min-h-32 items-center justify-center px-4 py-10">
|
||||
<x-loading text="Loading pull requests…" />
|
||||
</div>
|
||||
</div>
|
||||
</x-application.settings-section>
|
||||
@endif
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div wire:target="loadImages" wire:loading>
|
||||
<div class="w-full" wire:target="loadImages" wire:loading>
|
||||
<div class="flex items-center justify-center gap-2 px-4 py-10 text-[13px] text-neutral-500 dark:text-fg-dim">
|
||||
<x-loading class="size-4" />
|
||||
Loading available images…
|
||||
|
||||
Reference in new issue
Block a user