mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-19 00:15:01 +00:00
feat(domains): add inline indexing and redirect controls
Move domain indexing and redirect settings into domain management views and include noindex changes in deployment configuration diffs.
This commit is contained in:
1 parent
8c9a7413d0
commit
69ba001e21
22 files changed
+327
-185
No files matched your search
@@ -9,6 +9,7 @@
|
||||
'emptyText' => 'No options available.',
|
||||
'live' => false,
|
||||
'onChange' => null, // optional $wire method to call after a selection
|
||||
'onChangeArgs' => null, // optional arguments followed by the selected value
|
||||
'wire' => true, // false = purely client-side value (no Livewire binding)
|
||||
'value' => null, // initial value when wire=false
|
||||
'disabled' => false,
|
||||
@@ -55,7 +56,12 @@
|
||||
this.open = false;
|
||||
if (String(option.value) === String(this.value)) return;
|
||||
this.value = option.value;
|
||||
@if ($onChange) this.$nextTick(() => this.$wire.{{ $onChange }}()); @endif
|
||||
this.$dispatch('listbox-change', { value: option.value });
|
||||
@if ($onChange && is_array($onChangeArgs))
|
||||
this.$nextTick(() => this.$wire.{{ $onChange }}(...@js($onChangeArgs), option.value));
|
||||
@elseif ($onChange)
|
||||
this.$nextTick(() => this.$wire.{{ $onChange }}());
|
||||
@endif
|
||||
},
|
||||
toggle() {
|
||||
this.open = !this.open;
|
||||
@@ -102,8 +108,15 @@
|
||||
</button>
|
||||
@if ($portal)
|
||||
<template x-teleport="body">
|
||||
<div id="{{ $panelId }}" class="listbox-panel" style="position: fixed; z-index: 9999" x-show="open"
|
||||
<div id="{{ $panelId }}" class="listbox-panel"
|
||||
style="position: fixed; z-index: 9999; visibility: hidden" x-show="open && positioned"
|
||||
x-cloak :style="{ visibility: positioned ? 'visible' : 'hidden' }"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 -translate-y-1 scale-[0.98]"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 scale-100"
|
||||
x-transition:leave-end="opacity-0 -translate-y-1 scale-[0.98]"
|
||||
x-effect="if (open) requestAnimationFrame(() => positionPanel($el))" role="listbox">
|
||||
<div x-show="options.length === 0"
|
||||
class="px-3 py-2 text-[13px] text-neutral-500 dark:text-fg-dim">
|
||||
@@ -124,7 +137,13 @@
|
||||
</div>
|
||||
</template>
|
||||
@else
|
||||
<div x-ref="panel" class="listbox-panel" x-show="open" x-cloak role="listbox">
|
||||
<div x-ref="panel" class="listbox-panel" x-show="open" x-cloak
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 -translate-y-1 scale-[0.98]"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 scale-100"
|
||||
x-transition:leave-end="opacity-0 -translate-y-1 scale-[0.98]" role="listbox">
|
||||
<div x-show="options.length === 0"
|
||||
class="px-3 py-2 text-[13px] text-neutral-500 dark:text-fg-dim">
|
||||
{{ $emptyText }}
|
||||
|
||||
@@ -15,15 +15,16 @@
|
||||
domainSearch: '',
|
||||
modalOpen: @js($showEditDomainModal || $editDomainDnsFailed),
|
||||
editingServiceLabel: @js($editingService ?? ''),
|
||||
// Local-only until Save — never touch $wire on open/close (avoids Livewire toJSON proxy bugs).
|
||||
localEditingIndex: @js($editingIndex),
|
||||
localEditingDomain: @js($editingDomain),
|
||||
localEditingService: @js($editingService),
|
||||
openEditDomain(index, url, service) {
|
||||
localIndexing: 'index',
|
||||
openEditDomain(index, url, service, indexing) {
|
||||
this.localEditingIndex = index;
|
||||
this.localEditingDomain = url;
|
||||
this.localEditingService = service;
|
||||
this.editingServiceLabel = service || '';
|
||||
this.localIndexing = indexing || 'index';
|
||||
this.modalOpen = true;
|
||||
this.$nextTick(() => document.getElementById('editingDomainLocal')?.focus?.());
|
||||
},
|
||||
@@ -39,6 +40,7 @@
|
||||
$wire.editingIndex = this.localEditingIndex;
|
||||
$wire.editingDomain = this.localEditingDomain;
|
||||
$wire.editingService = this.localEditingService;
|
||||
$wire.editingIndexing = this.localIndexing;
|
||||
$wire.showEditDomainModal = true;
|
||||
},
|
||||
matchesDomainSearch(value) {
|
||||
@@ -48,7 +50,7 @@
|
||||
return values.some((value) => this.matchesDomainSearch(value));
|
||||
},
|
||||
}"
|
||||
@open-edit-domain.window="openEditDomain($event.detail.index, $event.detail.url, $event.detail.service)"
|
||||
@open-edit-domain.window="openEditDomain($event.detail.index, $event.detail.url, $event.detail.service, $event.detail.indexing)"
|
||||
@edit-domain-saved.window="closeEditDomain()">
|
||||
<x-application.settings-section id="domains-section" title="Domains" :helper="$helperText">
|
||||
@can('update', $application)
|
||||
@@ -81,16 +83,11 @@
|
||||
|
||||
@if (! $isCompose)
|
||||
@if ($labelsAreWritable)
|
||||
@if ($application->redirect === 'both')
|
||||
<x-forms.input label="Direction" value="Allow www & non-www" readonly
|
||||
helper="Readonly labels are disabled. You can set the direction in the labels section." />
|
||||
@elseif ($application->redirect === 'www')
|
||||
<x-forms.input label="Direction" value="Redirect to www" readonly
|
||||
helper="Readonly labels are disabled. You can set the direction in the labels section." />
|
||||
@elseif ($application->redirect === 'non-www')
|
||||
<x-forms.input label="Direction" value="Redirect to non-www" readonly
|
||||
helper="Readonly labels are disabled. You can set the direction in the labels section." />
|
||||
@endif
|
||||
<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">
|
||||
@@ -115,9 +112,10 @@
|
||||
@endif
|
||||
@elseif (! $labelsAreWritable && count($composeServices) > 0 && $composeDomainGroups->isNotEmpty())
|
||||
<p class="text-sm text-neutral-500 dark:text-fg-dim">
|
||||
Per-service www/non-www redirects are available next to each service group in the table below.
|
||||
Per-service www/non-www redirects are available next to each service group below.
|
||||
</p>
|
||||
@endif
|
||||
|
||||
</x-application.settings-section>
|
||||
|
||||
{{-- Toolbar --}}
|
||||
@@ -140,7 +138,9 @@
|
||||
</div>
|
||||
@endif
|
||||
@can('update', $application)
|
||||
@include('livewire.project.shared.cloudflare-autoconfigure')
|
||||
<div class="relative shrink-0">
|
||||
@include('livewire.project.shared.cloudflare-autoconfigure')
|
||||
</div>
|
||||
@unless ($labelsAreWritable)
|
||||
@if (! $isCompose || count($composeServices) > 0)
|
||||
<x-modal-input title="Add domain" :closeOutside="false" :wireIgnore="false"
|
||||
@@ -236,8 +236,7 @@
|
||||
@php
|
||||
$rows = $grouped->get($serviceName, collect());
|
||||
$redirectWireKey = $this->serviceRedirectWireKey($serviceName);
|
||||
$redirect = $serviceRedirects[$redirectWireKey] ?? 'both';
|
||||
$redirectLabel = match ($redirect) {
|
||||
$redirectLabel = match ($serviceRedirects[$redirectWireKey] ?? 'both') {
|
||||
'www' => 'Redirect to www',
|
||||
'non-www' => 'Redirect to non-www',
|
||||
default => 'Allow both',
|
||||
@@ -253,26 +252,14 @@
|
||||
</span>
|
||||
@unless ($labelsAreWritable)
|
||||
@can('update', $application)
|
||||
<div class="relative flex shrink-0 items-center gap-2 px-1 py-1 text-sm text-neutral-600 dark:text-fg-dim"
|
||||
wire:loading.class="opacity-50"
|
||||
wire:target="serviceRedirects.{{ $redirectWireKey }}">
|
||||
<span>{{ $redirectLabel }}</span>
|
||||
<x-reicon name="chevron-down" class="size-4 shrink-0"
|
||||
wire:loading.remove
|
||||
wire:target="serviceRedirects.{{ $redirectWireKey }}" />
|
||||
<x-loading-on-button wire:loading.delay
|
||||
wire:target="serviceRedirects.{{ $redirectWireKey }}" />
|
||||
<select id="application-compose-domain-redirect-{{ $redirectWireKey }}"
|
||||
wire:model.change="serviceRedirects.{{ $redirectWireKey }}"
|
||||
wire:change="setServiceRedirect(@js($serviceName))"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="serviceRedirects.{{ $redirectWireKey }}"
|
||||
class="absolute inset-0 size-full cursor-pointer opacity-0 disabled:cursor-wait"
|
||||
aria-label="Redirect direction for {{ $serviceName }}">
|
||||
<option value="both">Allow www & non-www</option>
|
||||
<option value="www">Redirect to www</option>
|
||||
<option value="non-www">Redirect to non-www</option>
|
||||
</select>
|
||||
<div class="w-52 shrink-0">
|
||||
<x-forms.listbox id="serviceRedirects.{{ $redirectWireKey }}"
|
||||
htmlId="application-compose-domain-redirect-{{ $redirectWireKey }}"
|
||||
live :options="[
|
||||
['value' => 'both', 'label' => 'Allow www & non-www'],
|
||||
['value' => 'www', 'label' => 'Redirect to www'],
|
||||
['value' => 'non-www', 'label' => 'Redirect to non-www'],
|
||||
]" wire:change="setServiceRedirect(@js($serviceName))" />
|
||||
</div>
|
||||
@else
|
||||
<span class="shrink-0 text-sm text-neutral-600 dark:text-fg-dim">{{ $redirectLabel }}</span>
|
||||
@@ -384,6 +371,14 @@
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
@unless ($labelsAreWritable)
|
||||
<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'],
|
||||
]" />
|
||||
@endunless
|
||||
|
||||
@if ($editDomainDnsFailed)
|
||||
<x-callout type="danger" title="DNS is not pointing to the right IP">
|
||||
This domain does not currently resolve to this server.
|
||||
|
||||
@@ -71,25 +71,6 @@
|
||||
<x-reicon name="settings" class="size-4" />
|
||||
</a>
|
||||
</div>
|
||||
@if ($buildPack !== 'dockercompose' && $application->settings->is_container_label_readonly_enabled && count($this->configuredDomains) > 0)
|
||||
<div class="mt-4 flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<h4>Search engine indexing</h4>
|
||||
<x-helper
|
||||
helper="Checked domains are served with an <span class='text-helper'>X-Robots-Tag: noindex, nofollow</span> response header, which keeps them out of search engines.<br><br>Useful for auto-generated or staging domains you do not want indexed, while your production domain stays indexable.<br><br>This header overrides any X-Robots-Tag your application sets itself.<br><br>Preview deployments are always noindex." />
|
||||
</div>
|
||||
@foreach ($this->configuredDomains as $domain)
|
||||
<label
|
||||
class="form-control flex max-w-full cursor-pointer flex-row items-center gap-4 py-1 pr-2 dark:hover:bg-coolgray-100">
|
||||
<span class="flex min-w-0 grow gap-2 break-words">{{ $domain }}</span>
|
||||
<input type="checkbox" value="{{ $domain }}" wire:model="noindexDomains"
|
||||
wire:change="updateNoindexDomains" wire:loading.attr="disabled"
|
||||
x-bind:disabled="!canUpdate"
|
||||
class="shrink-0 cursor-pointer rounded-sm text-coolgray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs focus-visible:ring-offset-2 dark:border-neutral-700 dark:bg-coolgray-100 dark:focus-visible:ring-warning dark:focus-visible:ring-offset-base dark:disabled:cursor-not-allowed dark:disabled:bg-base" />
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</section>
|
||||
|
||||
@if ($buildPack !== 'dockercompose')
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
index: {{ $index }},
|
||||
url: @js($row['url']),
|
||||
service: @js($row['service'] ?? null),
|
||||
indexing: @js($application->isDomainNoindexed($row['url']) ? 'noindex' : 'index'),
|
||||
})"
|
||||
class="icon-button shrink-0"
|
||||
title="Edit domain" aria-label="Edit domain">
|
||||
|
||||
@@ -22,11 +22,15 @@
|
||||
localEditingIndex: @js($editingIndex),
|
||||
localEditingDomain: @js($editingDomain),
|
||||
localEditingServiceApplicationId: @js($editingServiceApplicationId),
|
||||
openEditDomain(index, url, serviceApplicationId, serviceLabel) {
|
||||
localDirection: 'both',
|
||||
localIndexing: 'index',
|
||||
openEditDomain(index, url, serviceApplicationId, serviceLabel, direction, indexing) {
|
||||
this.localEditingIndex = index;
|
||||
this.localEditingDomain = url;
|
||||
this.localEditingServiceApplicationId = serviceApplicationId;
|
||||
this.editingServiceLabel = serviceLabel || '';
|
||||
this.localDirection = direction || 'both';
|
||||
this.localIndexing = indexing || 'index';
|
||||
this.modalOpen = true;
|
||||
this.$nextTick(() => document.getElementById('editingDomainLocal')?.focus?.());
|
||||
},
|
||||
@@ -41,6 +45,8 @@
|
||||
$wire.editingIndex = this.localEditingIndex;
|
||||
$wire.editingDomain = this.localEditingDomain;
|
||||
$wire.editingServiceApplicationId = this.localEditingServiceApplicationId;
|
||||
$wire.editingDirection = this.localDirection;
|
||||
$wire.editingIndexing = this.localIndexing;
|
||||
$wire.showEditDomainModal = true;
|
||||
},
|
||||
matchesDomainSearch(value) {
|
||||
@@ -50,7 +56,7 @@
|
||||
return values.some((value) => this.matchesDomainSearch(value));
|
||||
},
|
||||
}"
|
||||
@open-edit-domain.window="openEditDomain($event.detail.index, $event.detail.url, $event.detail.serviceApplicationId, $event.detail.serviceLabel)"
|
||||
@open-edit-domain.window="openEditDomain($event.detail.index, $event.detail.url, $event.detail.serviceApplicationId, $event.detail.serviceLabel, $event.detail.direction, $event.detail.indexing)"
|
||||
@edit-domain-saved.window="closeEditDomain()">
|
||||
<x-application.settings-section id="service-domains-section" title="Domains">
|
||||
@can('update', $service)
|
||||
@@ -94,7 +100,9 @@
|
||||
@endif
|
||||
@can('update', $service)
|
||||
@if ($serviceAppCount > 0)
|
||||
@include('livewire.project.shared.cloudflare-autoconfigure')
|
||||
<div class="relative shrink-0">
|
||||
@include('livewire.project.shared.cloudflare-autoconfigure')
|
||||
</div>
|
||||
<x-modal-input title="Add domain" :closeOutside="false" :wireIgnore="false"
|
||||
canGate="update" :canResource="$service">
|
||||
<x-slot:content>
|
||||
@@ -177,31 +185,12 @@
|
||||
@php
|
||||
$app = collect($serviceApps)->firstWhere('id', (int) $appId);
|
||||
$heading = \Illuminate\Support\Str::headline($app['name'] ?? $rows->first()['service_name'] ?? 'Service');
|
||||
$redirect = $serviceRedirects[$appId] ?? 'both';
|
||||
$redirectLabel = match ($redirect) {
|
||||
'www' => 'Redirect to www',
|
||||
'non-www' => 'Redirect to non-www',
|
||||
default => 'Allow both',
|
||||
};
|
||||
@endphp
|
||||
<section id="service-domain-group-{{ $appId }}" wire:key="service-domain-group-{{ $appId }}"
|
||||
x-show="matchesDomainSearch(@js($heading.' '.$rows->pluck('url')->implode(' ')))"
|
||||
class="border-b border-neutral-200 last:border-b-0 dark:border-white/10">
|
||||
<div class="flex w-full items-center gap-3 px-4 py-3">
|
||||
<div class="flex w-full items-center gap-3 border-b border-neutral-200 bg-neutral-50 px-4 py-3 dark:border-white/10 dark:bg-white/[0.04]">
|
||||
<span class="min-w-0 flex-1 truncate text-sm font-medium text-black dark:text-white">{{ $heading }}</span>
|
||||
@can('update', $service)
|
||||
<div class="w-52 shrink-0"
|
||||
wire:loading.class="opacity-50" wire:target="serviceRedirects.{{ $appId }}">
|
||||
<x-forms.listbox id="serviceRedirects.{{ $appId }}"
|
||||
htmlId="service-domain-redirect-{{ $appId }}" live :options="[
|
||||
['value' => 'both', 'label' => 'Allow www & non-www'],
|
||||
['value' => 'www', 'label' => 'Redirect to www'],
|
||||
['value' => 'non-www', 'label' => 'Redirect to non-www'],
|
||||
]" />
|
||||
</div>
|
||||
@else
|
||||
<span class="shrink-0 text-sm text-neutral-600 dark:text-fg-dim">{{ $redirectLabel }}</span>
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
<div wire:key="service-domain-rows-{{ $appId }}-{{ md5(serialize($rows->all())) }}">
|
||||
@@ -210,7 +199,7 @@
|
||||
'domainRows' => $domainRows,
|
||||
'service' => $service,
|
||||
'showServiceColumn' => false,
|
||||
'showHeader' => false,
|
||||
'showHeader' => true,
|
||||
])
|
||||
</div>
|
||||
</section>
|
||||
@@ -279,6 +268,20 @@
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<x-forms.listbox id="edit-service-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'],
|
||||
]" />
|
||||
<x-forms.listbox id="edit-service-domain-indexing" label="Search engine indexing"
|
||||
:wire="false" value="index" x-model="localIndexing" portal :options="[
|
||||
['value' => 'index', 'label' => 'Indexable'],
|
||||
['value' => 'noindex', 'label' => 'Noindex'],
|
||||
]" />
|
||||
</div>
|
||||
|
||||
@if ($editDomainDnsFailed)
|
||||
<x-callout type="danger" title="DNS is not pointing to the right IP">
|
||||
This domain does not currently resolve to this server.
|
||||
|
||||
@@ -16,26 +16,6 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if (count($this->configuredDomains) > 0)
|
||||
<div class="flex flex-col gap-2 pt-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<h4>Search Engine Indexing</h4>
|
||||
<x-helper
|
||||
helper="Checked domains are served with an <span class='text-helper'>X-Robots-Tag: noindex, nofollow</span> response header, which keeps them out of search engines.<br><br>Useful for auto-generated or staging domains you do not want indexed, while your production domain stays indexable.<br><br>This header overrides any X-Robots-Tag your service sets itself." />
|
||||
</div>
|
||||
@foreach ($this->configuredDomains as $domain)
|
||||
<label
|
||||
class="form-control flex max-w-full cursor-pointer flex-row items-center gap-4 py-1 pr-2 dark:hover:bg-coolgray-100">
|
||||
<span class="flex min-w-0 grow gap-2 break-words">{{ $domain }}</span>
|
||||
<input type="checkbox" value="{{ $domain }}" wire:model="noindexDomains"
|
||||
wire:change="updateNoindexDomains" wire:loading.attr="disabled"
|
||||
@cannot('update', $application) disabled @endcannot
|
||||
class="shrink-0 cursor-pointer rounded-sm text-coolgray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs focus-visible:ring-offset-2 dark:border-neutral-700 dark:bg-coolgray-100 dark:focus-visible:ring-warning dark:focus-visible:ring-offset-base dark:disabled:cursor-not-allowed dark:disabled:bg-base" />
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<x-domain-conflict-modal :conflicts="$domainConflicts" :showModal="$showDomainConflictModal" confirmAction="confirmDomainUsage">
|
||||
<x-slot:consequences>
|
||||
<ul class="mt-2 ml-4 list-disc">
|
||||
|
||||
@@ -131,9 +131,11 @@
|
||||
@click="$dispatch('open-edit-domain', {
|
||||
index: {{ $index }},
|
||||
url: @js($row['url']),
|
||||
serviceApplicationId: {{ (int) ($row['service_application_id'] ?? 0) }},
|
||||
serviceLabel: @js($serviceLabel),
|
||||
})"
|
||||
serviceApplicationId: {{ (int) ($row['service_application_id'] ?? 0) }},
|
||||
serviceLabel: @js($serviceLabel),
|
||||
direction: @js($serviceRedirects[$row['service_application_id']] ?? 'both'),
|
||||
indexing: @js($service->applications->firstWhere('id', $row['service_application_id'])?->isDomainNoindexed($row['url']) ? 'noindex' : 'index'),
|
||||
})"
|
||||
class="icon-button shrink-0" title="Edit domain" aria-label="Edit domain">
|
||||
<x-reicon name="settings" class="size-3.5" />
|
||||
</button>
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
x-bind:aria-expanded="dnsEntriesOpen" title="DNS entries for this server">
|
||||
<x-reicon name="globe" class="size-3.5" />
|
||||
DNS entries
|
||||
<span class="inline-flex transition-transform" :class="dnsEntriesOpen && 'rotate-180'">
|
||||
<x-reicon name="chevron-down" class="size-3 opacity-55" />
|
||||
</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
|
||||
stroke="currentColor" class="size-3.5 shrink-0 opacity-60">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m8 9 4-4 4 4m0 6-4 4-4-4" />
|
||||
</svg>
|
||||
</button>
|
||||
<div x-show="dnsEntriesOpen" x-cloak role="menu" x-transition.origin.top.right
|
||||
class="listbox-panel left-auto! right-0! z-[90]! w-56! min-w-56!">
|
||||
|
||||
Reference in new issue
Block a user