mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-18 08:14:35 +00:00
feat(domains): add structured URL editing and responsive domain controls
Add reusable domain URL parsing and input components, expose per-domain redirect and indexing controls, and improve application and service domain layouts across responsive breakpoints.
This commit is contained in:
1 parent
61a2b183d3
commit
32bf1860d4
30 files changed
+693
-292
No files matched your search
@@ -18,15 +18,11 @@
|
||||
localEditingIndex: @js($editingIndex),
|
||||
localEditingDomain: @js($editingDomain),
|
||||
localEditingService: @js($editingService),
|
||||
localDirection: 'both',
|
||||
localIndexing: 'index',
|
||||
openEditDomain(index, url, service, indexing, direction) {
|
||||
openEditDomain(index, url, service) {
|
||||
this.localEditingIndex = index;
|
||||
this.localEditingDomain = url;
|
||||
this.localEditingService = service;
|
||||
this.editingServiceLabel = service || '';
|
||||
this.localDirection = direction || 'both';
|
||||
this.localIndexing = indexing || 'index';
|
||||
this.modalOpen = true;
|
||||
this.$nextTick(() => document.getElementById('editingDomainLocal')?.focus?.());
|
||||
},
|
||||
@@ -42,8 +38,6 @@
|
||||
$wire.editingIndex = this.localEditingIndex;
|
||||
$wire.editingDomain = this.localEditingDomain;
|
||||
$wire.editingService = this.localEditingService;
|
||||
$wire.editingDirection = this.localDirection;
|
||||
$wire.editingIndexing = this.localIndexing;
|
||||
$wire.showEditDomainModal = true;
|
||||
},
|
||||
matchesDomainSearch(value) {
|
||||
@@ -53,7 +47,7 @@
|
||||
return values.some((value) => this.matchesDomainSearch(value));
|
||||
},
|
||||
}"
|
||||
@open-edit-domain.window="openEditDomain($event.detail.index, $event.detail.url, $event.detail.service, $event.detail.indexing, $event.detail.direction)"
|
||||
@open-edit-domain.window="openEditDomain($event.detail.index, $event.detail.url, $event.detail.service)"
|
||||
@edit-domain-saved.window="closeEditDomain()">
|
||||
<x-application.settings-section id="domains-section" title="Domains" :helper="$helperText">
|
||||
@can('update', $application)
|
||||
@@ -84,37 +78,6 @@
|
||||
</x-callout>
|
||||
@endcannot
|
||||
|
||||
@if (! $isCompose)
|
||||
@if ($labelsAreWritable)
|
||||
<x-forms.input label="Direction" value="{{ match ($application->redirect) {
|
||||
'www' => 'Redirect to www',
|
||||
'non-www' => 'Redirect to non-www',
|
||||
default => 'Allow www & non-www',
|
||||
} }}" readonly helper="Readonly labels are disabled. You can set the direction in the labels section." />
|
||||
@else
|
||||
<div class="flex w-full flex-col gap-3 sm:flex-row sm:items-end">
|
||||
<div class="min-w-0 flex-1">
|
||||
<x-forms.listbox id="redirect" label="Direction" required :options="[
|
||||
['value' => 'both', 'label' => 'Allow www & non-www'],
|
||||
['value' => 'www', 'label' => 'Redirect to www'],
|
||||
['value' => 'non-www', 'label' => 'Redirect to non-www'],
|
||||
]" helper="Add <strong>both</strong> www and non-www in Coolify. Both hostnames must resolve to this server so the proxy can serve or redirect them. Do not use a DNS-provider URL redirect record for the non-canonical host; Coolify handles the HTTP redirect. Changes apply when you click Set direction."
|
||||
:disabled="! auth()->user()?->can('update', $application)" />
|
||||
</div>
|
||||
@can('update', $application)
|
||||
<div class="w-full shrink-0 sm:w-auto">
|
||||
<x-modal-confirmation title="Confirm redirection setting?" buttonTitle="Set direction"
|
||||
submitAction="setRedirect" :actions="['All traffic will be redirected to the selected direction.']"
|
||||
confirmationText="{{ ($application->fqdn ?: 'domains') . '/' }}"
|
||||
confirmationLabel="Please confirm the execution of the action by entering the Application URL below"
|
||||
shortConfirmationLabel="Application URL" :confirmWithPassword="false"
|
||||
step2ButtonText="Set direction" canGate="update" :canResource="$application" />
|
||||
</div>
|
||||
@endcan
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
</x-application.settings-section>
|
||||
|
||||
{{-- Toolbar --}}
|
||||
@@ -153,16 +116,15 @@
|
||||
</x-slot:content>
|
||||
<form wire:submit="addDomain" class="application-settings-form flex flex-col gap-4">
|
||||
@if ($isCompose && count($composeServices) > 0)
|
||||
<x-forms.select label="Service" id="newDomainService" required>
|
||||
@foreach ($composeServices as $serviceName)
|
||||
<option value="{{ $serviceName }}">{{ $serviceName }}</option>
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
<x-forms.listbox label="Service" id="newDomainService" required
|
||||
:options="collect($composeServices)->map(fn ($serviceName) => [
|
||||
'value' => $serviceName,
|
||||
'label' => $serviceName,
|
||||
])->values()->all()"
|
||||
:disabled="! auth()->user()->can('update', $application)" />
|
||||
@endif
|
||||
|
||||
<x-forms.input id="newDomain" label="Domain URL" placeholder="https://app.example.com"
|
||||
helper="Full URL including scheme. Optional path and container port are supported.<br><br><span class='text-helper'>Examples</span><br>- https://app.coolify.io<br>- https://app.coolify.io/api/v3<br>- https://app.coolify.io:3000<br>- https://app.coolify.io:8080/api"
|
||||
required />
|
||||
<x-forms.domain-input id="newDomain" />
|
||||
|
||||
@if ($addDomainDnsFailed)
|
||||
<x-callout type="danger" title="DNS is not pointing to the right IP">
|
||||
@@ -252,6 +214,8 @@
|
||||
<span>Domain</span>
|
||||
<span>DNS</span>
|
||||
<span>Last checked</span>
|
||||
<span>Search engine indexing</span>
|
||||
<span>Direction</span>
|
||||
<span></span>
|
||||
</div>
|
||||
@foreach ($rows as $row)
|
||||
@@ -287,6 +251,8 @@
|
||||
<span>Domain</span>
|
||||
<span>DNS Check</span>
|
||||
<span>Last checked</span>
|
||||
<span>Search engine indexing</span>
|
||||
<span>Direction</span>
|
||||
<span></span>
|
||||
</div>
|
||||
@foreach ($domainRows as $index => $row)
|
||||
@@ -340,40 +306,8 @@
|
||||
<input type="text" class="input" readonly x-bind:value="editingServiceLabel" />
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<div class="mb-1.5 flex h-4 w-full items-center gap-1.5">
|
||||
<label class="mb-0! flex items-center gap-1 text-sm font-medium leading-4" for="editingDomainLocal">
|
||||
Domain URL <x-highlighted text="*" />
|
||||
</label>
|
||||
</div>
|
||||
<input id="editingDomainLocal" type="url" class="input" required
|
||||
placeholder="https://app.example.com"
|
||||
x-model="localEditingDomain" />
|
||||
<p class="mt-1 text-[12px] leading-5 text-neutral-500 dark:text-fg-dim">
|
||||
Full URL including scheme. Optional path and container port are supported.
|
||||
</p>
|
||||
@error('editingDomain')
|
||||
<p class="mt-1 text-[12px] text-red-500">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
@unless ($labelsAreWritable)
|
||||
<div class="grid gap-4 {{ $isCompose ? 'sm:grid-cols-2' : '' }}">
|
||||
@if ($isCompose)
|
||||
<x-forms.listbox id="edit-domain-direction" label="Direction"
|
||||
:wire="false" value="both" x-model="localDirection" portal :options="[
|
||||
['value' => 'both', 'label' => 'Allow www & non-www'],
|
||||
['value' => 'www', 'label' => 'Redirect to www'],
|
||||
['value' => 'non-www', 'label' => 'Redirect to non-www'],
|
||||
]" />
|
||||
@endif
|
||||
<x-forms.listbox id="edit-domain-indexing" label="Search engine indexing"
|
||||
:wire="false" value="index" x-model="localIndexing" portal :options="[
|
||||
['value' => 'index', 'label' => 'Indexable'],
|
||||
['value' => 'noindex', 'label' => 'Noindex'],
|
||||
]" />
|
||||
</div>
|
||||
@endunless
|
||||
<x-forms.domain-input id="editingDomainLocal" errorId="editingDomain" :wire="false"
|
||||
x-model="localEditingDomain" />
|
||||
|
||||
@if ($editDomainDnsFailed)
|
||||
<x-callout type="danger" title="DNS is not pointing to the right IP">
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
}
|
||||
}">
|
||||
<form wire:submit='submit' class="application-settings-form flex flex-col">
|
||||
<x-unsaved-bar action="submit" />
|
||||
<x-unsaved-bar action="submit"
|
||||
targets="name,description,buildPack,staticImage,baseDirectory,dockerComposeLocation,dockerComposeCustomBuildCommand,dockerComposeCustomStartCommand,watchPaths,dockerfileLocation,dockerfileTargetBuild,publishDirectory,installCommand,buildCommand,startCommand,customNginxConfiguration,dockerfile,dockerRegistryImageName,dockerRegistryImageTag,portsExposes,portsMappings,customNetworkAliases,customDockerRunOptions,httpBasicAuthUsername,httpBasicAuthPassword,preDeploymentCommand,preDeploymentCommandContainer,postDeploymentCommand,postDeploymentCommandContainer,isContainerLabelReadonlyEnabled,isContainerLabelEscapeEnabled,customLabels" />
|
||||
<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')
|
||||
@@ -302,7 +303,7 @@
|
||||
@endif
|
||||
@if ($buildPack === 'dockercompose')
|
||||
<div x-data="{ showRaw: true }" class="mt-5">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="mb-2 flex items-center justify-between 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,6 +17,28 @@
|
||||
? \Illuminate\Support\Carbon::parse($row['checked_at'])->diffForHumans()
|
||||
: null;
|
||||
$gridClass = ($isCompose ?? false) ? 'domains-table-grid-compose' : 'domains-table-grid';
|
||||
$domainParts = $isSuggested ? null : parse_url($row['url']);
|
||||
$faviconUrl = is_array($domainParts) && isset($domainParts['scheme'], $domainParts['host'])
|
||||
? $domainParts['scheme'].'://'.$domainParts['host'].(isset($domainParts['port']) ? ':'.$domainParts['port'] : '').'/favicon.ico'
|
||||
: null;
|
||||
$redirectPairKey = function (string $url): string {
|
||||
$parts = parse_url($url);
|
||||
if (! is_array($parts) || ! isset($parts['host'])) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$host = preg_replace('/^www\./i', '', $parts['host']);
|
||||
|
||||
return strtolower(($parts['scheme'] ?? '').'://'.$host.':'.($parts['port'] ?? '').($parts['path'] ?? ''));
|
||||
};
|
||||
$pairKey = $redirectPairKey($row['url']);
|
||||
$firstPairRowIndex = collect($domainRows)
|
||||
->reject(fn ($item) => (bool) ($item['is_suggested'] ?? false))
|
||||
->filter(fn ($item) => ($item['service'] ?? null) === ($row['service'] ?? null))
|
||||
->filter(fn ($item) => $redirectPairKey($item['url']) === $pairKey)
|
||||
->keys()
|
||||
->first();
|
||||
$showDirection = ! $isSuggested && $firstPairRowIndex === $index;
|
||||
@endphp
|
||||
|
||||
<div wire:key="domain-row-{{ $index }}-{{ md5(($isSuggested ? 's:' : '') . $row['url'] . '|' . ($row['service'] ?? '')) }}"
|
||||
@@ -25,6 +47,7 @@
|
||||
'data-table-row',
|
||||
$gridClass,
|
||||
'domains-row-suggested' => $isSuggested,
|
||||
'domains-row-without-direction' => ! $showDirection,
|
||||
])>
|
||||
<div class="flex min-w-0 flex-col gap-1">
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
||||
@@ -35,6 +58,11 @@
|
||||
{{ $row['url'] }}
|
||||
</span>
|
||||
@else
|
||||
@if ($faviconUrl)
|
||||
<img src="{{ $faviconUrl }}" alt="" loading="lazy" decoding="async"
|
||||
referrerpolicy="no-referrer" x-on:error="$el.remove()"
|
||||
class="size-4 shrink-0 rounded-sm" />
|
||||
@endif
|
||||
<a href="{{ getFqdnWithoutPort($row['url']) }}" target="_blank"
|
||||
class="min-w-0 text-[13px] text-black underline decoration-neutral-300 underline-offset-2 hover:decoration-coollabs sm:truncate dark:text-fg dark:decoration-white/20 dark:hover:decoration-warning"
|
||||
title="{{ $row['url'] }}">
|
||||
@@ -78,6 +106,51 @@
|
||||
{{ $checkedAt ?: '-' }}
|
||||
</div>
|
||||
|
||||
<div class="min-w-0" title="Search engine indexing">
|
||||
@unless ($isSuggested)
|
||||
<span class="domains-mobile-label">Search engine indexing</span>
|
||||
@endunless
|
||||
@if ($isSuggested)
|
||||
<span class="text-[13px] text-neutral-500 dark:text-fg-dim">-</span>
|
||||
@elseif (auth()->user()?->can('update', $application) && ! $labelsAreWritable)
|
||||
<x-forms.listbox id="domain-indexing-{{ $index }}" :wire="false"
|
||||
:value="$application->isDomainNoindexed($row['url']) ? 'noindex' : 'index'"
|
||||
onChange="toggleNoindexDomain" :onChangeArgs="[$row['url']]" portal :options="[
|
||||
['value' => 'index', 'label' => 'Indexable'],
|
||||
['value' => 'noindex', 'label' => 'Noindex'],
|
||||
]" />
|
||||
@else
|
||||
<span class="text-[13px] text-neutral-500 dark:text-fg-dim">
|
||||
{{ $application->isDomainNoindexed($row['url']) ? 'Noindex' : 'Indexable' }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="min-w-0" title="Direction">
|
||||
@php
|
||||
$rowDirection = $domainDirection ?? $redirect;
|
||||
$directionLabel = match ($rowDirection) {
|
||||
'www' => 'Redirect to www',
|
||||
'non-www' => 'Redirect to non-www',
|
||||
default => 'Allow both',
|
||||
};
|
||||
@endphp
|
||||
@if ($showDirection)
|
||||
<span class="domains-mobile-label">Direction</span>
|
||||
@endif
|
||||
@if ($showDirection && auth()->user()?->can('update', $application) && ! $labelsAreWritable)
|
||||
<x-forms.listbox id="domain-direction-{{ $index }}" :wire="false" :value="$rowDirection"
|
||||
:onChange="$isCompose ? 'updateServiceRedirect' : 'updateRedirect'"
|
||||
:onChangeArgs="$isCompose ? [$row['service']] : []" portal :options="[
|
||||
['value' => 'both', 'label' => 'Allow www & non-www'],
|
||||
['value' => 'www', 'label' => 'Redirect to www'],
|
||||
['value' => 'non-www', 'label' => 'Redirect to non-www'],
|
||||
]" />
|
||||
@elseif ($showDirection)
|
||||
<span class="text-[13px] text-neutral-500 dark:text-fg-dim">{{ $directionLabel }}</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
@can('update', $application)
|
||||
<button type="button" wire:click="checkDomainDns({{ $index }})"
|
||||
@@ -107,8 +180,6 @@
|
||||
index: {{ $index }},
|
||||
url: @js($row['url']),
|
||||
service: @js($row['service'] ?? null),
|
||||
indexing: @js($application->isDomainNoindexed($row['url']) ? 'noindex' : 'index'),
|
||||
direction: @js($domainDirection ?? $redirect),
|
||||
})"
|
||||
class="icon-button shrink-0"
|
||||
title="Edit domain" aria-label="Edit domain">
|
||||
|
||||
Reference in new issue
Block a user