feat: Add domains sidebar view for simple applications

Implement a dedicated Domains management view for non-Docker Compose applications with:

- Separate Domains sidebar item under application configuration
- List-based domain management (add/remove individual domains)
- Three-field domain input (scheme, domain, port) with searchable dropdown for schemes
- Support for custom protocols (tcp, grpc, ftp, ssh, etc.) via x-forms.datalist component
- Generate random domain functionality
- DNS validation per domain
- Domain conflict detection
- Modal dialogs for adding/editing domains matching Coolify's design patterns
- Authorization checks using AuthorizesRequests trait
- Read-only domain display in General view for simple applications

Features:
- DomainAdd component: Creates new domains via searchable scheme dropdown
- DomainEdit component: Parses and updates existing domains
- Domains component: Manages domain list with add/edit/remove/refresh actions
- Backend stores domains as comma-separated strings in applications.fqdn column
- Frontend UI uses dashboard box styling with proper dark mode support

Documentation:
- Added x-forms.datalist component documentation to CLAUDE.md
- Added Domain Management section with architecture details
- Documented DomainAdd/DomainEdit components and workflow

🧠 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Andras BacsaiandClaude committed 2025-12-27 15:23:33 +01:00
1 parent 083d745d70
commit 3a08d26dc9
10 files changed
+795 -51

No files matched your search

+132
View File
@@ -353,6 +353,138 @@ This file contains high-level guidelines for Claude Code. For **more detailed, t
- Use `gap` utilities for spacing, not margins
## UI Components
### Available Blade Components
#### Callout Component
Use `x-callout` for alert/warning/info messages:
```html
<x-callout type="warning" title="Warning Title">
Your message here
</x-callout>
```
**Available types**: `warning`, `danger`, `info`, `success`
**DO NOT use**: `x-forms.alert` (does not exist)
**Example usage**:
```html
<x-callout type="warning" title="Note" class="mb-4">
This view is for simple applications only.
</x-callout>
```
The callout component is located at `resources/views/components/callout.blade.php` and provides:
- Automatic icon selection based on type
- Color-coded styling (yellow for warning, red for danger, blue for info, green for success)
- Dark mode support
- Consistent padding and border styling
#### Datalist Component (Searchable Dropdown)
Use `x-forms.datalist` for searchable dropdowns with custom input capability:
```html
<x-forms.datalist wire:model="scheme" label="Scheme" placeholder="Select or type" required
helper="Select from options or type a custom value">
<option value="https">HTTPS</option>
<option value="http">HTTP</option>
<option value="wss">WSS (WebSocket Secure)</option>
<option value="ws">WS (WebSocket)</option>
</x-forms.datalist>
```
**Key Features**:
- **Searchable**: Filter options by typing
- **Custom input**: Users can type values not in the list
- **Single or multiple**: Set `:multiple="true"` for multi-select
- **Keyboard navigation**: Full keyboard support
- **Visual feedback**: Shows selected state and loading indicators
- **Dark mode support**: Automatic theming
**When to use**:
- ✅ User needs to search through many options (e.g., server selection, SSH keys)
- ✅ Custom input is allowed/encouraged (e.g., protocol schemes: http, https, custom protocols)
- ✅ Multi-select functionality needed
- ❌ Simple yes/no or 2-3 options → use `x-forms.select` instead
**DO NOT use**: HTML5 `<datalist>` directly or `list=""` attribute on inputs. Always use `x-forms.datalist` component.
**Example - Protocol scheme selector**:
```html
{{-- Provides common schemes but allows custom (tcp, grpc, ftp, etc.) --}}
<x-forms.datalist wire:model="scheme" label="Scheme" placeholder="Select or type a scheme" required
helper="Select from common schemes or type a custom one (e.g., tcp, grpc, ftp)">
<option value="https">HTTPS</option>
<option value="http">HTTP</option>
<option value="wss">WSS (WebSocket Secure)</option>
<option value="ws">WS (WebSocket)</option>
</x-forms.datalist>
```
**Example - Multi-select SSH keys**:
```html
<x-forms.datalist label="Additional SSH Keys" wire:model="selectedSshKeyIds"
:multiple="true" placeholder="Select SSH keys...">
@foreach ($sshKeys as $key)
<option value="{{ $key->id }}">{{ $key->name }}</option>
@endforeach
</x-forms.datalist>
```
The datalist component is located at `resources/views/components/forms/datalist.blade.php` and provides:
- Alpine.js-powered search and filtering
- Livewire integration with `@entangle` for reactive state
- Visual tags for multi-select mode
- Dropdown with hover effects
- Handles both string and integer values
- Required/readonly/disabled states
## Domain Management
### Application Domains
- Domains are stored as **comma-separated strings** in the `applications.fqdn` column (type: `longText`, nullable)
- The `Application` model has an `fqdns()` accessor that converts the comma-separated string to an array
- Example: `"http://app.coolify.io,https://cloud.coolify.io/dashboard"`
### New Domains View (Simple Applications)
A dedicated Domains management view is available for non-Docker Compose applications:
- **Route**: `/domains` under application configuration
- **Component**: `App\Livewire\Project\Application\Domains`
- **View**: `resources/views/livewire/project/application/domains.blade.php`
- **Sidebar**: Only shown when `$application->build_pack !== 'dockercompose'`
**Features**:
- List-based domain management (add/remove individual domains)
- Generate random domain functionality
- DNS validation per domain
- Domain conflict detection
- Redirect settings (www/non-www)
- Authorization using `AuthorizesRequests` trait
- **Three-field domain input**: Scheme (searchable dropdown), Domain (text), Port (optional number)
- Scheme uses `x-forms.datalist` component for searchable dropdown with custom input
- Supports common protocols (http, https, ws, wss) and custom protocols (tcp, grpc, ftp, ssh, etc.)
- Backend stores as single URL string: `scheme://domain:port`
**Domain Add/Edit Components**:
- **DomainAdd**: `App\Livewire\Project\Application\DomainAdd`
- Properties: `$scheme`, `$domain`, `$port`
- Combines fields into full URL: `$scheme . '://' . $domain . ':' . $port`
- Validation: `alpha_dash` for scheme (allows any alphanumeric + dash/underscore)
- Uses modal pattern with `x-modal-input` component
- **DomainEdit**: `App\Livewire\Project\Application\DomainEdit`
- Parses existing URL using PHP's `parse_url()`
- Extracts scheme, host (with path), and port into separate fields
- Updates domain at specific index in array
**General View Changes**:
- Domain fields in General view are now read-only for simple applications
- Displays current domains with a "Manage domains →" link to the new Domains view
- Docker Compose applications continue to manage domains per service in the General view
Random other things you should remember:
- App\Models\Application::team must return a relationship instance., always use team()
- Always use `Model::ownedByCurrentTeamCached()` instead of `Model::ownedByCurrentTeam()->get()` for team-scoped queries to avoid duplicate database queries
@@ -0,0 +1,116 @@
<?php
namespace App\Livewire\Project\Application;
use App\Models\Application;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
use Spatie\Url\Url;
class DomainAdd extends Component
{
use AuthorizesRequests;
public Application $application;
public string $scheme = 'https';
public string $domain = '';
public string $port = '';
public function mount()
{
$this->authorize('update', $this->application);
}
public function submit()
{
$this->authorize('update', $this->application);
$this->validate([
'scheme' => 'required|string|alpha_dash',
'domain' => 'required|string',
'port' => 'nullable|integer|min:1|max:65535',
]);
try {
// Build the full domain URL
$fullDomain = $this->scheme.'://'.trim($this->domain);
// Add port if specified
if (! empty($this->port)) {
$fullDomain .= ':'.$this->port;
}
// Clean and validate
$fullDomain = str($fullDomain)->replaceEnd(',', '')->trim()->toString();
$fullDomain = str($fullDomain)->replaceStart(',', '')->trim()->toString();
// Validate URL format (allow any scheme)
try {
Url::fromString($fullDomain);
} catch (\Throwable $e) {
$this->dispatch('error', 'Invalid URL format.');
return;
}
$fullDomain = str($fullDomain)->lower()->toString();
// Get current domains
$currentDomains = $this->application->fqdn
? array_values(array_filter(explode(',', $this->application->fqdn)))
: [];
// Check if domain already exists
if (in_array($fullDomain, $currentDomains)) {
$this->dispatch('error', 'Domain already exists in the list.');
return;
}
// Add to domains array
$currentDomains[] = $fullDomain;
// Check for domain conflicts
$this->application->fqdn = implode(',', $currentDomains);
$result = checkDomainUsage(resource: $this->application);
if ($result['hasConflicts']) {
$this->dispatch('domainConflict', [
'conflicts' => $result['conflicts'],
'newDomain' => $fullDomain,
]);
return;
}
// Save to database
$this->application->save();
// Reset default labels
if ($this->application->settings->is_container_label_readonly_enabled) {
$customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
$this->application->custom_labels = base64_encode($customLabels);
$this->application->save();
}
$this->dispatch('configurationChanged');
$this->dispatch('success', 'Domain added successfully.');
$this->dispatch('modalClosed');
$this->dispatch('refreshDomains');
// Reset inputs
$this->scheme = 'https';
$this->domain = '';
$this->port = '';
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.project.application.domain-add');
}
}
@@ -0,0 +1,142 @@
<?php
namespace App\Livewire\Project\Application;
use App\Models\Application;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
use Spatie\Url\Url;
class DomainEdit extends Component
{
use AuthorizesRequests;
public Application $application;
public int $index;
public string $domain;
public string $scheme = 'https';
public string $domainName = '';
public string $port = '';
public function mount()
{
$this->authorize('update', $this->application);
// Parse the existing domain into scheme, domain, and port
$this->parseDomain($this->domain);
}
protected function parseDomain(string $fullDomain)
{
try {
$url = parse_url($fullDomain);
// Extract scheme
$this->scheme = $url['scheme'] ?? 'https';
// Extract host/path
$this->domainName = $url['host'] ?? '';
if (isset($url['path']) && $url['path'] !== '/') {
$this->domainName .= $url['path'];
}
// Extract port
$this->port = isset($url['port']) ? (string) $url['port'] : '';
} catch (\Throwable $e) {
// If parsing fails, just use the domain as-is
$this->domainName = $fullDomain;
}
}
public function submit()
{
$this->authorize('update', $this->application);
$this->validate([
'scheme' => 'required|string|alpha_dash',
'domainName' => 'required|string',
'port' => 'nullable|integer|min:1|max:65535',
]);
try {
// Build the full domain URL
$newDomain = $this->scheme.'://'.trim($this->domainName);
// Add port if specified
if (! empty($this->port)) {
$newDomain .= ':'.$this->port;
}
// Clean and validate
$newDomain = str($newDomain)->replaceEnd(',', '')->trim()->toString();
$newDomain = str($newDomain)->replaceStart(',', '')->trim()->toString();
// Validate URL format (allow any scheme)
try {
Url::fromString($newDomain);
} catch (\Throwable $e) {
$this->dispatch('error', 'Invalid URL format.');
return;
}
$newDomain = str($newDomain)->lower()->toString();
// Get current domains
$currentDomains = $this->application->fqdn
? array_values(array_filter(explode(',', $this->application->fqdn)))
: [];
// Check if domain already exists (but not the one being edited)
$existingDomains = $currentDomains;
unset($existingDomains[$this->index]);
if (in_array($newDomain, $existingDomains)) {
$this->dispatch('error', 'Domain already exists in the list.');
return;
}
// Update the domain at the specified index
$currentDomains[$this->index] = $newDomain;
// Check for domain conflicts
$this->application->fqdn = implode(',', $currentDomains);
$result = checkDomainUsage(resource: $this->application);
if ($result['hasConflicts']) {
$this->dispatch('domainConflict', [
'conflicts' => $result['conflicts'],
'editedDomain' => $newDomain,
]);
return;
}
// Save to database
$this->application->save();
// Reset default labels
if ($this->application->settings->is_container_label_readonly_enabled) {
$customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
$this->application->custom_labels = base64_encode($customLabels);
$this->application->save();
}
$this->dispatch('configurationChanged');
$this->dispatch('success', 'Domain updated successfully.');
$this->dispatch('modalClosed');
$this->dispatch('refreshDomains');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.project.application.domain-edit');
}
}
@@ -0,0 +1,206 @@
<?php
namespace App\Livewire\Project\Application;
use App\Models\Application;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Domains extends Component
{
use AuthorizesRequests;
public Application $application;
public array $domains = [];
public $domainConflicts = [];
public $showDomainConflictModal = false;
public $forceSaveDomains = false;
public string $redirect;
protected $listeners = [
'confirmDomainUsage',
'refreshDomains' => 'loadDomains',
'configurationChanged' => '$refresh',
];
public function mount()
{
$this->authorize('view', $this->application);
$this->redirect = $this->application->redirect;
$this->loadDomains();
}
public function loadDomains(): void
{
$this->application->refresh();
if ($this->application->fqdn) {
$this->domains = array_values(array_filter(
explode(',', $this->application->fqdn),
fn ($domain) => ! empty(trim($domain))
));
} else {
$this->domains = [];
}
}
public function removeDomain(int $index)
{
$this->authorize('update', $this->application);
try {
if (isset($this->domains[$index])) {
unset($this->domains[$index]);
$this->domains = array_values($this->domains); // Re-index array
$this->saveDomains();
$this->dispatch('success', 'Domain removed successfully.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function generateDomain()
{
$this->authorize('update', $this->application);
try {
$server = data_get($this->application, 'destination.server');
if ($server) {
$uuid = new Cuid2;
$fqdn = generateUrl(server: $server, random: $uuid);
// Check if domain already exists
if (in_array($fqdn, $this->domains)) {
$this->dispatch('error', 'Generated domain already exists.');
return;
}
$this->domains[] = $fqdn;
if (! $this->saveDomains()) {
return; // Stop if there are conflicts
}
$this->dispatch('success', 'Domain generated successfully.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function checkDNS(string $domain)
{
$this->authorize('view', $this->application);
try {
if ($this->application->additional_servers->count() === 0) {
if (! validateDNSEntry($domain, $this->application->destination->server)) {
$this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$domain->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
} else {
$this->dispatch('success', 'DNS validation successful.');
}
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
protected function saveDomains(): bool
{
try {
$this->authorize('update', $this->application);
// Convert array back to comma-separated string
$fqdn = implode(',', array_filter($this->domains));
// Check for domain conflicts if not forcing save
if (! $this->forceSaveDomains && ! empty($fqdn)) {
$this->application->fqdn = $fqdn;
$result = checkDomainUsage(resource: $this->application);
if ($result['hasConflicts']) {
$this->domainConflicts = $result['conflicts'];
$this->showDomainConflictModal = true;
return false;
}
} else {
// Reset the force flag after using it
$this->forceSaveDomains = false;
}
// Save to database
$this->application->fqdn = $fqdn ?: null;
$this->application->save();
// Reset default labels
$this->resetDefaultLabels();
// Reload domains to ensure consistency
$this->application->refresh();
$this->loadDomains();
$this->dispatch('configurationChanged');
return true;
} catch (\Throwable $e) {
handleError($e, $this);
return false;
}
}
public function confirmDomainUsage()
{
$this->forceSaveDomains = true;
$this->showDomainConflictModal = false;
$this->saveDomains();
}
public function setRedirect()
{
$this->authorize('update', $this->application);
try {
$has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count();
if ($has_www === 0 && $this->redirect === 'www') {
$this->dispatch('error', 'You want to redirect to www, but you do not have a www domain set.<br><br>Please add www to your domain list and as an A DNS record (if applicable).');
return;
}
$this->application->redirect = $this->redirect;
$this->application->save();
$this->resetDefaultLabels();
$this->dispatch('success', 'Redirect updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
protected function resetDefaultLabels()
{
try {
if ($this->application->settings->is_container_label_readonly_enabled) {
$customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
$this->application->custom_labels = base64_encode($customLabels);
$this->application->save();
}
} catch (\Throwable $e) {
// Silent fail for label regeneration
}
}
public function render()
{
return view('livewire.project.application.domains');
}
}
@@ -10,7 +10,11 @@
<div class="flex flex-col items-start gap-2 min-w-fit">
<a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
href="{{ route('project.application.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">General</a>
<a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
@if ($application->build_pack !== 'dockercompose')
<a class='menu-item' wire:current.exact="menu-item-active"
href="{{ route('project.application.domains', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Domains</a>
@endif
<a class='menu-item' wire:current.exact="menu-item-active"
href="{{ route('project.application.advanced', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Advanced</a>
@if ($application->destination->server->isSwarm())
<a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
@@ -79,6 +83,8 @@
<div class="w-full">
@if ($currentRoute === 'project.application.configuration')
<livewire:project.application.general :application="$application" />
@elseif ($currentRoute === 'project.application.domains' && $application->build_pack !== 'dockercompose')
<livewire:project.application.domains :application="$application" />
@elseif ($currentRoute === 'project.application.swarm' && $application->destination->server->isSwarm())
<livewire:project.application.swarm :application="$application" />
@elseif ($currentRoute === 'project.application.advanced')
@@ -0,0 +1,19 @@
<form class="flex flex-col w-full gap-2 rounded-sm" wire:submit='submit'>
<x-forms.datalist wire:model="scheme" label="Scheme" placeholder="Select or type a scheme" required
helper="Select from common schemes or type a custom one (e.g., tcp, grpc, ftp)">
<option value="https">https</option>
<option value="http">http</option>
<option value="wss">wss (WebSocket Secure)</option>
<option value="ws">ws (WebSocket)</option>
</x-forms.datalist>
<x-forms.input wire:model="domain" label="Domain" placeholder="app.example.com" required
helper="Enter the domain name without protocol or port.<br><br><span class='text-helper'>Example</span><br>app.coolify.io or cloud.coolify.io/dashboard" />
<x-forms.input wire:model="port" label="Port (Optional)" placeholder="Default: 80 (http) / 443 (https)"
helper="Leave empty to use default port. Common ports: 80 (HTTP), 443 (HTTPS), 3000, 8080" />
<x-forms.button @click="modalOpen=false" type="submit">
Add Domain
</x-forms.button>
</form>
@@ -0,0 +1,19 @@
<form class="flex flex-col w-full gap-2 rounded-sm" wire:submit='submit'>
<x-forms.datalist wire:model="scheme" label="Scheme" placeholder="Select or type a scheme" required
helper="Select from common schemes or type a custom one (e.g., tcp, grpc, ftp)">
<option value="https">https</option>
<option value="http">http</option>
<option value="wss">wss (WebSocket Secure)</option>
<option value="ws">ws (WebSocket)</option>
</x-forms.datalist>
<x-forms.input wire:model="domainName" label="Domain" placeholder="app.example.com" required
helper="Enter the domain name without protocol or port.<br><br><span class='text-helper'>Example</span><br>app.coolify.io or cloud.coolify.io/dashboard" />
<x-forms.input wire:model="port" label="Port (Optional)" placeholder="Default: 80 (http) / 443 (https)"
helper="Leave empty to use default port. Common ports: 80 (HTTP), 443 (HTTPS), 3000, 8080" />
<x-forms.button @click="modalOpen=false" type="submit">
Update Domain
</x-forms.button>
</form>
@@ -0,0 +1,120 @@
<div>
<div class="flex items-center gap-2">
<h2>Domains</h2>
@can('update', $application)
<x-modal-input buttonTitle="+ Add" title="New Domain">
<livewire:project.application.domain-add :application="$application" />
</x-modal-input>
<x-forms.button wire:click="generateDomain">Generate Domain</x-forms.button>
@endcan
</div>
<div class="pb-4">Manage domains for your application.</div>
@if ($application->build_pack === 'dockercompose')
<x-callout type="warning" title="Note" class="mb-4">
This view is for simple applications only. Docker Compose applications manage domains per service in the General view.
</x-callout>
@endif
@if (empty($domains))
<div class="flex flex-col items-center justify-center py-8 text-center">
<svg class="w-16 h-16 mb-4 text-neutral-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
</svg>
<h3 class="mb-2">No domains configured</h3>
<p class="text-sm text-neutral-500 dark:text-neutral-400">
Add a domain to make your application accessible via a custom URL.
</p>
</div>
@else
<div class="flex flex-col gap-2 pb-6">
@foreach ($domains as $index => $domain)
<div class="flex items-center gap-4 p-2 min-h-[4rem] rounded-sm border border-neutral-200 dark:border-coolgray-300 bg-white dark:bg-coolgray-100 shadow-sm">
{{-- Check Icon --}}
<div class="flex-shrink-0">
<svg class="w-6 h-6 text-success" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
{{-- Domain Name --}}
<div class="flex-1 min-w-0">
<div class="font-mono text-base break-all text-black dark:text-white">{{ $domain }}</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">Valid Configuration</div>
</div>
{{-- Action Buttons --}}
<div class="flex gap-2">
@can('view', $application)
<x-forms.button wire:click="checkDNS('{{ $domain }}')" title="Check DNS">
Refresh
</x-forms.button>
@endcan
@can('update', $application)
<x-modal-input buttonTitle="Edit" title="Edit Domain">
<livewire:project.application.domain-edit :application="$application" :index="$index"
:domain="$domain" :key="'edit-' . $index" />
</x-modal-input>
<x-modal-confirmation title="Remove Domain?"
message="Are you sure you want to remove this domain: {{ $domain }}?"
buttonTitle="Remove" submitAction="removeDomain({{ $index }})"
:confirmWithText="false" :confirmWithPassword="false" isErrorButton>
<x-slot:button>
<x-forms.button isError title="Remove domain">
Remove
</x-forms.button>
</x-slot:button>
</x-modal-confirmation>
@endcan
</div>
</div>
@endforeach
</div>
<div class="pt-4">
<h3>Redirect Settings</h3>
<div class="pb-2 text-sm">Configure how www and non-www domains should be handled.</div>
@if ($application->settings->is_container_label_readonly_enabled == false)
@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
@else
<div class="flex items-end gap-2">
<x-forms.select wire:model="redirect" label="Direction" required
helper="You must need to add www and non-www as an A DNS record. Make sure the www domain is added under your domains list.">
<option value="both">Allow www & non-www.</option>
<option value="www">Redirect to www.</option>
<option value="non-www">Redirect to non-www.</option>
</x-forms.select>
@can('update', $application)
<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 . '/' }}"
confirmationLabel="Please confirm the execution of the action by entering the Application URL below"
shortConfirmationLabel="Application URL" :confirmWithPassword="false"
step2ButtonText="Set Direction">
<x-slot:customButton>
<div class="w-[7.2rem]">Set Direction</div>
</x-slot:customButton>
</x-modal-confirmation>
@endcan
</div>
@endif
</div>
@endif
{{-- Domain Conflict Modal --}}
<x-domain-conflict-modal :conflicts="$domainConflicts" :showModal="$showDomainConflictModal"
confirmAction="confirmDomainUsage" />
</div>
@@ -100,58 +100,41 @@
@endif
</div>
@if ($buildPack !== 'dockercompose')
<div class="flex items-end gap-2">
@if ($application->settings->is_container_label_readonly_enabled == false)
<x-forms.input placeholder="https://coolify.io" wire:model="fqdn" label="Domains" readonly
helper="Readonly labels are disabled. You can set the domains in the labels section."
x-bind:disabled="!canUpdate" />
@else
<x-forms.input placeholder="https://coolify.io" wire:model="fqdn" label="Domains"
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "
x-bind:disabled="!canUpdate" />
@can('update', $application)
<x-forms.button wire:click="getWildcardDomain">Generate Domain
</x-forms.button>
@endcan
@endif
<div class="flex items-center gap-2 pt-4">
<h3>Domains</h3>
<a class="text-sm underline"
href="{{ route('project.application.domains', ['project_uuid' => request()->route('project_uuid'), 'environment_uuid' => request()->route('environment_uuid'), 'application_uuid' => $application->uuid]) }}">
Manage domains →
</a>
</div>
<div class="flex items-end gap-2">
@if ($application->settings->is_container_label_readonly_enabled == false)
@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."
x-bind:disabled="!canUpdate" />
@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."
x-bind:disabled="!canUpdate" />
@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."
x-bind:disabled="!canUpdate" />
@endif
<div class="pb-4">
@if (!empty($application->fqdn))
@php
$allDomains = $application->fqdns;
$displayLimit = 3;
$displayDomains = array_slice($allDomains, 0, $displayLimit);
$remainingCount = count($allDomains) - $displayLimit;
@endphp
<div class="flex flex-wrap gap-2">
@foreach ($displayDomains as $domain)
@php
$host = parse_url($domain, PHP_URL_HOST) ?? $domain;
@endphp
<span class="px-2 py-1 text-xs font-mono rounded bg-neutral-100 dark:bg-coolgray-300 text-neutral-700 dark:text-neutral-300 truncate max-w-[200px]" title="{{ $domain }}">
{{ $host }}
</span>
@endforeach
@if ($remainingCount > 0)
<a href="{{ route('project.application.domains', ['project_uuid' => request()->route('project_uuid'), 'environment_uuid' => request()->route('environment_uuid'), 'application_uuid' => $application->uuid]) }}"
class="px-2 py-1 text-xs rounded bg-neutral-200 dark:bg-coolgray-200 text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200">
+{{ $remainingCount }} more
</a>
@endif
</div>
@else
<x-forms.select label="Direction" id="redirect" required
helper="You must need to add www and non-www as an A DNS record. Make sure the www domain is added under Domains."
x-bind:disabled="!canUpdate">
<option value="both">Allow www & non-www.</option>
<option value="www">Redirect to www.</option>
<option value="non-www">Redirect to non-www.</option>
</x-forms.select>
@if ($application->settings->is_container_label_readonly_enabled)
@can('update', $application)
<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 . '/' }}"
confirmationLabel="Please confirm the execution of the action by entering the Application URL below"
shortConfirmationLabel="Application URL" :confirmWithPassword="false"
step2ButtonText="Set Direction">
<x-slot:customButton>
<div class="w-[7.2rem]">Set Direction</div>
</x-slot:customButton>
</x-modal-confirmation>
@endcan
@endif
<div class="text-sm text-neutral-500 dark:text-neutral-400">
No domains configured. Add domains in the Domains view.
</div>
@endif
</div>
@endif
+1
View File
@@ -193,6 +193,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/', ApplicationConfiguration::class)->name('project.application.configuration');
Route::get('/swarm', ApplicationConfiguration::class)->name('project.application.swarm');
Route::get('/advanced', ApplicationConfiguration::class)->name('project.application.advanced');
Route::get('/domains', ApplicationConfiguration::class)->name('project.application.domains');
Route::get('/environment-variables', ApplicationConfiguration::class)->name('project.application.environment-variables');
Route::get('/persistent-storage', ApplicationConfiguration::class)->name('project.application.persistent-storage');
Route::get('/source', ApplicationConfiguration::class)->name('project.application.source');