feat(ui): Shadow UI redesign, domain management, and DNS autoconfigure (#11119)

Co-authored-by: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com>
This commit is contained in:
Andras BacsaiandShadowArcanist authored and GitHub committed 2026-08-03 23:11:54 +02:00
1 parent 0b843bb07c
commit 8ae56587d5
497 files changed
+44045 -22630

No files matched your search

@@ -1,257 +1,242 @@
<div>
<div class="flex flex-col gap-6">
<livewire:project.application.preview.form :application="$application" />
@if (count($application->additional_servers) > 0)
<div class="pb-4">Previews will be deployed on <span
class="dark:text-warning">{{ $application->destination->server->name }}</span>.</div>
<x-callout type="info" title="Preview deployment server">
Preview deployments run on {{ $application->destination->server->name }}.
</x-callout>
@endif
<div>
@if ($application->is_github_based())
<div class="flex items-center gap-2">
@if ($application->is_github_based())
<x-application.settings-section id="preview-pull-requests-section" title="Pull requests"
helper="Load open pull requests from GitHub, then configure or deploy a preview." flush>
<x-slot:actions>
@isset($rate_limit_remaining)
<span class="text-xs text-neutral-500 dark:text-fg-dim">
{{ $rate_limit_remaining }} requests remaining
</span>
@endisset
@can('update', $application)
<h3>Pull Requests on Git</h3>
<x-forms.button wire:click="load_prs">Load Pull Requests
<x-forms.button wire:click="load_prs">
Load pull requests
</x-forms.button>
@endcan
</x-slot:actions>
<div wire:loading.remove wire:target="load_prs">
@forelse ($pull_requests as $pull_request)
<div
class="flex flex-col gap-3 border-b border-neutral-200 px-4 py-3.5 last:border-b-0 sm:flex-row sm:items-center dark:border-white/[0.07]">
<div
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-neutral-100 font-mono text-xs font-semibold text-neutral-600 ring-1 ring-neutral-200 dark:bg-white/[0.05] dark:text-fg-dim dark:ring-white/[0.07]">
#{{ data_get($pull_request, 'number') }}
</div>
<div class="min-w-0 flex-1">
<h4 class="truncate text-sm font-semibold text-black dark:text-fg">
{{ data_get($pull_request, 'title') }}
</h4>
<a target="_blank"
class="mt-1 inline-flex items-center gap-1 text-xs text-neutral-500 hover:text-coollabs dark:text-fg-dim dark:hover:text-warning"
href="{{ data_get($pull_request, 'html_url') }}">
Open on GitHub
<x-external-link />
</a>
</div>
<div class="flex shrink-0 items-center gap-2">
@can('update', $application)
<x-forms.button
wire:click="add('{{ data_get($pull_request, 'number') }}', '{{ data_get($pull_request, 'html_url') }}')">
Configure
</x-forms.button>
@endcan
@can('deploy', $application)
<x-forms.button
wire:click="add_and_deploy('{{ data_get($pull_request, 'number') }}', '{{ data_get($pull_request, 'html_url') }}')">
Deploy preview
</x-forms.button>
@endcan
</div>
</div>
@empty
<x-empty size="sm" title="No pull requests loaded"
description="Load open pull requests from GitHub to configure a preview deployment."
icon-name="sources" />
@endforelse
</div>
@endif
@isset($rate_limit_remaining)
<div class="pt-1 pb-4">Requests remaining till rate limited by Git: {{ $rate_limit_remaining }}</div>
@endisset
<div wire:loading.remove wire:target='load_prs'>
@if ($pull_requests->count() > 0)
<div class="overflow-x-auto table-md">
<table>
<thead>
<tr>
<th>PR Number</th>
<th>PR Title</th>
<th>Git</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach ($pull_requests as $pull_request)
<tr>
<th>{{ data_get($pull_request, 'number') }}</th>
<td>{{ data_get($pull_request, 'title') }}</td>
<td>
<a target="_blank" class="text-xs"
href="{{ data_get($pull_request, 'html_url') }}">Open PR on
Git
<x-external-link />
</a>
</td>
<td class="flex flex-col gap-1 md:flex-row">
@can('update', $application)
<x-forms.button
wire:click="add('{{ data_get($pull_request, 'number') }}', '{{ data_get($pull_request, 'html_url') }}')">
Configure
</x-forms.button>
@endcan
@can('deploy', $application)
<x-forms.button
wire:click="add_and_deploy('{{ data_get($pull_request, 'number') }}', '{{ data_get($pull_request, 'html_url') }}')">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>Deploy
</x-forms.button>
@endcan
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
</div>
</div>
</x-application.settings-section>
@endif
@if ($application->build_pack === 'dockerimage')
<div class="flex flex-col gap-2 pt-4">
<h3>Manual Preview Deployment</h3>
<form wire:submit.prevent="addDockerImagePreview" class="flex flex-col gap-2 xl:flex-row xl:items-end">
<x-forms.input id="manualPullRequestId" label="Pull Request Id"
helper="Used as the preview identifier for naming, domains, logs, and cleanup." />
<x-forms.input id="manualDockerTag" label="Docker Tag"
helper="The image tag to deploy for this preview, for example pr_1234." />
<x-application.settings-section id="manual-preview-section" title="Manual preview"
helper="Deploy a preview directly from a Docker image tag.">
<form wire:submit.prevent="addDockerImagePreview"
class="grid gap-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] md:items-end">
<x-forms.input id="manualPullRequestId" label="Preview ID"
helper="Used for domains, logs, container names, and cleanup." />
<x-forms.input id="manualDockerTag" label="Docker tag"
helper="For example, pr_1234." />
@can('deploy', $application)
<x-forms.button type="submit">Deploy Preview</x-forms.button>
<x-forms.button type="submit">Deploy preview</x-forms.button>
@endcan
</form>
</div>
</x-application.settings-section>
@endif
@if ($application->previews->count() > 0)
<h3 class="py-4">Deployments</h3>
<div class="flex flex-wrap w-full gap-4">
@foreach (data_get($application, 'previews') as $previewName => $preview)
<div class="flex flex-col w-full p-4 border dark:border-coolgray-200"
wire:key="preview-container-{{ $preview->pull_request_id }}">
<div class="flex gap-2">PR #{{ data_get($preview, 'pull_request_id') }} |
@if (str(data_get($preview, 'status'))->startsWith('running'))
<x-status.running :status="data_get($preview, 'status')" />
@elseif(str(data_get($preview, 'status'))->startsWith('restarting'))
<x-status.restarting :status="data_get($preview, 'status')" />
@else
<x-status.stopped :status="data_get($preview, 'status')" />
@endif
@if (data_get($preview, 'status') !== 'exited')
| <a target="_blank" href="{{ data_get($preview, 'fqdn') }}">Open Preview
<x-external-link />
</a>
@endif
@if (filled(data_get($preview, 'pull_request_html_url')))
|
<a target="_blank" href="{{ data_get($preview, 'pull_request_html_url') }}">Open
PR on Git
<x-external-link />
</a>
@endif
@if (count($parameters) > 0)
|
<a {{ wireNavigate() }}
href="{{ route('project.application.deployment.index', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
Deployment Logs
</a>
|
<a {{ wireNavigate() }}
href="{{ route('project.application.logs', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
Application Logs
</a>
@endif
<x-application.settings-section id="preview-deployments-section" title="Preview deployments"
helper="Manage domains, deployments, logs, and lifecycle actions for configured previews." flush>
@forelse (data_get($application, 'previews') as $previewName => $preview)
@php
$previewStatus = str(data_get($preview, 'status'));
$previewIsRunning = $previewStatus->startsWith('running');
$previewIsRestarting = $previewStatus->startsWith('restarting');
$previewIsStopped = $previewStatus->startsWith('exited');
@endphp
<section class="border-b border-neutral-200 p-4 last:border-b-0 dark:border-white/[0.07]"
wire:key="preview-container-{{ $preview->pull_request_id }}">
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div class="flex min-w-0 items-center gap-3">
<div
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-neutral-100 font-mono text-xs font-semibold text-neutral-600 ring-1 ring-neutral-200 dark:bg-white/[0.05] dark:text-fg-dim dark:ring-white/[0.07]">
#{{ data_get($preview, 'pull_request_id') }}
</div>
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-2">
<h4 class="text-sm font-semibold text-black dark:text-fg">
Preview #{{ data_get($preview, 'pull_request_id') }}
</h4>
@if ($previewIsRunning)
<x-status.running :status="data_get($preview, 'status')" />
@elseif ($previewIsRestarting)
<x-status.restarting :status="data_get($preview, 'status')" />
@else
<x-status.stopped :status="data_get($preview, 'status')" />
@endif
</div>
<div class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
@if (!$previewIsStopped && filled(data_get($preview, 'fqdn')))
<a target="_blank" class="text-neutral-500 hover:text-coollabs dark:text-fg-dim dark:hover:text-warning"
href="{{ data_get($preview, 'fqdn') }}">Open preview</a>
@endif
@if (filled(data_get($preview, 'pull_request_html_url')))
<a target="_blank" class="text-neutral-500 hover:text-coollabs dark:text-fg-dim dark:hover:text-warning"
href="{{ data_get($preview, 'pull_request_html_url') }}">Open pull request</a>
@endif
@if (count($parameters) > 0)
<a {{ wireNavigate() }} class="text-neutral-500 hover:text-coollabs dark:text-fg-dim dark:hover:text-warning"
href="{{ route('project.application.deployment.index', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
Deployment logs
</a>
<a {{ wireNavigate() }} class="text-neutral-500 hover:text-coollabs dark:text-fg-dim dark:hover:text-warning"
href="{{ route('project.application.logs', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
Application logs
</a>
@endif
</div>
</div>
</div>
@if ($application->build_pack === 'dockercompose')
<div class="flex flex-col gap-4 pt-4">
@if (collect(json_decode($preview->docker_compose_domains))->count() === 0)
<form wire:submit="save_preview('{{ $preview->id }}')"
class="flex items-end gap-2 pt-4">
<x-forms.input label="Domain" helper="One domain per preview."
id="previewFqdns.{{ $previewName }}" canGate="update" :canResource="$application"></x-forms.input>
@can('update', $application)
<x-forms.button type="submit">Save</x-forms.button>
<x-forms.button wire:click="generate_preview('{{ $preview->id }}')">Generate
Domain</x-forms.button>
@endcan
</form>
@else
@foreach (collect(json_decode($preview->docker_compose_domains)) as $serviceName => $service)
<livewire:project.application.previews-compose
wire:key="preview-{{ $preview->pull_request_id }}-{{ $serviceName }}"
:service="$service" :serviceName="$serviceName" :preview="$preview" />
@endforeach
@endif
</div>
@else
<form wire:submit="save_preview('{{ $preview->id }}')" class="flex items-end gap-2 pt-4">
<x-forms.input label="Domain" helper="One domain per preview."
id="previewFqdns.{{ $previewName }}" canGate="update" :canResource="$application"></x-forms.input>
@if ($application->build_pack === 'dockerimage')
<x-forms.input label="Docker Tag" helper="The image tag used for this preview deployment."
id="previewDockerTags.{{ $previewName }}" canGate="update" :canResource="$application"></x-forms.input>
@endif
@can('update', $application)
<x-forms.button type="submit">Save</x-forms.button>
<x-forms.button wire:click="generate_preview('{{ $preview->id }}')">Generate
Domain</x-forms.button>
@endcan
</form>
@endif
<div class="flex flex-col xl:flex-row xl:items-center gap-2 pt-6">
<div class="flex-1"></div>
<div class="flex flex-wrap items-center gap-2 lg:justify-end">
@can('deploy', $application)
<x-forms.button
wire:click="force_deploy_without_cache({{ data_get($preview, 'pull_request_id') }})">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M12.983 8.978c3.955 -.182 7.017 -1.446 7.017 -2.978c0 -1.657 -3.582 -3 -8 -3c-1.661 0 -3.204 .19 -4.483 .515m-2.783 1.228c-.471 .382 -.734 .808 -.734 1.257c0 1.22 1.944 2.271 4.734 2.74" />
<path
d="M4 6v6c0 1.657 3.582 3 8 3c.986 0 1.93 -.067 2.802 -.19m3.187 -.82c1.251 -.53 2.011 -1.228 2.011 -1.99v-6" />
<path d="M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4" />
<path d="M3 3l18 18" />
</svg>
Force deploy (without
cache)
Rebuild
</x-forms.button>
<x-forms.button
wire:click="deploy({{ data_get($preview, 'pull_request_id') }}, null, false, '{{ data_get($preview, 'docker_registry_image_tag') }}')">
@if (data_get($preview, 'status') === 'exited')
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
@else
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-orange-400"
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
</path>
<path d="M7.05 11.038v-3.988"></path>
</svg> Redeploy
@endif
{{ $previewIsStopped ? 'Deploy' : 'Redeploy' }}
</x-forms.button>
@endcan
@if (data_get($preview, 'status') !== 'exited')
@if (!$previewIsStopped)
@can('deploy', $application)
<x-modal-confirmation title="Confirm Preview Deployment Stopping?" buttonTitle="Stop"
submitAction="stop({{ data_get($preview, 'pull_request_id') }})" :actions="[
<x-modal-confirmation title="Stop preview deployment?" buttonTitle="Stop"
submitAction="stop({{ data_get($preview, 'pull_request_id') }})"
:actions="[
'This preview deployment will be stopped.',
'If the preview deployment is currently in use data could be lost.',
'All non-persistent data of this preview deployment (containers, networks, unused images) will be deleted (don\'t worry, no data is lost and you can start the preview deployment again).',
'All non-persistent preview data will be removed.',
]"
:confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Stop Preview Deployment">
:confirmWithText="false" :confirmWithPassword="false"
step2ButtonText="Stop preview deployment">
<x-slot:customButton>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error"
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
<x-reicon name="stop" class="size-4 text-error" />
Stop
</x-slot:customButton>
</x-modal-confirmation>
@endcan
@endif
@can('delete', $application)
<x-modal-confirmation title="Confirm Preview Deployment Deletion?" buttonTitle="Delete"
<x-modal-confirmation title="Delete preview deployment?" buttonTitle="Delete"
isErrorButton submitAction="delete({{ data_get($preview, 'pull_request_id') }})"
:actions="[
'All containers of this preview deployment will be stopped and permanently deleted.',
]" confirmationText="{{ data_get($preview, 'fqdn') . '/' }}"
confirmationLabel="Please confirm the execution of the actions by entering the Preview Deployment name below"
shortConfirmationLabel="Preview Deployment Name" :confirmWithPassword="false" />
:actions="['All containers for this preview deployment will be stopped and permanently deleted.']"
confirmationText="{{ data_get($preview, 'fqdn') . '/' }}"
confirmationLabel="Enter the preview deployment name to confirm deletion"
shortConfirmationLabel="Preview deployment name" :confirmWithPassword="false" />
@endcan
</div>
</div>
@endforeach
</div>
@endif
<x-domain-conflict-modal
:conflicts="$domainConflicts"
:showModal="$showDomainConflictModal"
<div class="mt-4 border-t border-neutral-200 pt-4 dark:border-white/[0.07]">
@if ($application->build_pack === 'dockercompose')
@if (collect(json_decode($preview->docker_compose_domains))->count() === 0)
<form wire:submit="save_preview('{{ $preview->id }}')"
class="grid gap-3 md:grid-cols-[minmax(0,1fr)_auto] md:items-end">
<x-forms.input label="Domain" helper="One domain per preview."
id="previewFqdns.{{ $previewName }}" canGate="update"
:canResource="$application"
wire:change="save_preview('{{ $preview->id }}')" />
@can('update', $application)
<x-forms.button wire:click="generate_preview('{{ $preview->id }}')">
Generate domain
</x-forms.button>
@endcan
</form>
@else
<div class="flex flex-col gap-3">
@foreach (collect(json_decode($preview->docker_compose_domains)) as $serviceName => $service)
<livewire:project.application.previews-compose
wire:key="preview-{{ $preview->pull_request_id }}-{{ $serviceName }}"
:service="$service" :serviceName="$serviceName" :preview="$preview" />
@endforeach
</div>
@endif
@else
<form wire:submit="save_preview('{{ $preview->id }}')"
class="grid gap-3 {{ $application->build_pack === 'dockerimage'
? 'md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto]'
: 'md:grid-cols-[minmax(0,1fr)_auto]' }} md:items-end">
<x-forms.input label="Domain" helper="One domain per preview."
id="previewFqdns.{{ $previewName }}" canGate="update"
:canResource="$application"
wire:change="save_preview('{{ $preview->id }}')" />
@if ($application->build_pack === 'dockerimage')
<x-forms.input label="Docker tag" helper="Image tag used by this preview."
id="previewDockerTags.{{ $previewName }}" canGate="update"
:canResource="$application"
wire:change="save_preview('{{ $preview->id }}')" />
@endif
@can('update', $application)
<x-forms.button wire:click="generate_preview('{{ $preview->id }}')">
Generate domain
</x-forms.button>
@endcan
</form>
@endif
</div>
</section>
@empty
<x-empty title="No preview deployments"
description="Configure a pull request or manual preview to create an isolated deployment."
icon-name="eye" />
@endforelse
</x-application.settings-section>
<x-domain-conflict-modal :conflicts="$domainConflicts" :showModal="$showDomainConflictModal"
confirmAction="confirmDomainUsage">
The preview deployment domain is already in use by other resources. Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.
The preview deployment domain is already used by another resource and may cause routing conflicts.
<x-slot:consequences>
<ul class="mt-2 ml-4 list-disc">
<li>The preview deployment may not be accessible</li>
<li>Conflicts with production or other preview deployments</li>
<li>SSL certificates might not work correctly</li>
<li>Unpredictable routing behavior</li>
<li>The preview deployment may not be accessible.</li>
<li>SSL certificates may not work correctly.</li>
<li>Requests may be routed unpredictably.</li>
</ul>
</x-slot:consequences>
</x-domain-conflict-modal>