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:
Andras Bacsai committed 2026-08-10 15:10:30 +02:00
1 parent 8c9a7413d0
commit 69ba001e21
22 files changed
+327 -185

No files matched your search

@@ -58,6 +58,42 @@ it('stores a diff between successful deployments', function () {
->and(data_get($secondDeployment->configuration_diff, 'changes.0.label'))->toBe('Build command');
});
it('reports noindex domain changes as requiring a redeploy', function () {
$application = configurationChangedTestApplication([
'fqdn' => 'https://app.example.com,https://staging.example.com',
]);
$deployment = configurationChangedDeployment($application);
$application->markDeploymentConfigurationApplied($deployment);
$application->setNoindexDomains(['https://staging.example.com']);
$application->save();
$diff = $application->refresh()->pendingDeploymentConfigurationDiff();
$change = collect($diff->changes())->firstWhere('key', 'domains.noindex_domains');
expect($diff->isChanged())->toBeTrue()
->and($change)->not->toBeNull()
->and($change['label'])->toBe('Search engine indexing')
->and($change['impact'])->toBe('redeploy');
});
it('does not flag applications whose older snapshot omitted noindex domains', function () {
$application = configurationChangedTestApplication([
'fqdn' => 'https://app.example.com',
]);
$deployment = configurationChangedDeployment($application);
$application->markDeploymentConfigurationApplied($deployment);
$snapshot = $deployment->refresh()->configuration_snapshot;
$snapshot['sections']['domains']['items'] = collect($snapshot['sections']['domains']['items'])
->reject(fn (array $item): bool => $item['key'] === 'noindex_domains')
->values()
->all();
$deployment->update(['configuration_snapshot' => $snapshot]);
expect($application->refresh()->pendingDeploymentConfigurationDiff()->isChanged())->toBeFalse();
});
it('checks legacy preview deployment configuration hash using preview environment variable query', function () {
$application = configurationChangedTestApplication();
+25 -4
View File
@@ -271,6 +271,8 @@ it('updates a domain in place via modal', function () {
->call('startEdit', 0)
->assertSet('showEditDomainModal', true)
->assertSet('editingDomain', 'https://old.example.com')
->assertSee('Direction')
->assertSee('Search engine indexing')
->set('editingDomain', 'https://new.example.com')
->call('updateDomain')
->assertHasNoErrors()
@@ -375,7 +377,8 @@ it('auto-adds missing www counterpart as a normal domain when setting www redire
->assertSet('domainRows.0.is_suggested', false)
->assertSet('domainRows.1.is_suggested', false)
->assertSet('domainRows.0.url', 'https://example.com')
->assertSet('domainRows.1.url', 'https://www.example.com');
->assertSet('domainRows.1.url', 'https://www.example.com')
->assertSet('domainRows.1.checked_at', fn (?string $checkedAt): bool => filled($checkedAt));
$this->application->refresh();
@@ -1216,9 +1219,8 @@ it('uses the compact service domains layout for compose applications', function
expect($view)
->toContain('application-compose-domain-group-{{ $redirectWireKey }}')
->toContain('class="application-settings-section-body mt-1 scroll-mt-28')
->toContain('aria-label="Redirect direction for {{ $serviceName }}"')
->toContain('wire:target="serviceRedirects.{{ $redirectWireKey }}"')
->not->toContain('wire:target="serviceRedirects.{{ $redirectWireKey }},setServiceRedirect"')
->toContain('htmlId="application-compose-domain-redirect-{{ $redirectWireKey }}"')
->not->toContain('aria-label="Redirect direction for {{ $serviceName }}"')
->not->toContain('title="No domains for this service"');
});
@@ -1227,6 +1229,8 @@ it('provides client-side search for compose service domains', function () {
expect($view)
->toContain('x-model="domainSearch"')
->toContain('class="ml-auto flex flex-wrap items-center gap-2"')
->toContain('<div class="relative shrink-0">')
->toContain('placeholder="Search services or domains"')
->toContain('x-show="matchesDomainSearch(')
->toContain('title="No domains found"')
@@ -1326,3 +1330,20 @@ it('uses compose service redirect for suggested domain messaging when direction
->and($suggested['suggestion_role'] ?? null)->toBe('pair')
->and($suggested['url'] ?? null)->toBe('https://www.web.example.com');
});
it('updates search engine indexing from the domains view', function () {
$this->application->update(['fqdn' => 'https://app.example.com,https://staging.example.com']);
Livewire::test(Domains::class, ['application' => $this->application->fresh()])
->assertSee('Noindex')
->assertSee('Indexable')
->assertSee('x-model="localIndexing"', false)
->assertDontSee('@change="$wire.toggleNoindexDomain', false)
->assertDontSee('@js(', false)
->call('toggleNoindexDomain', 'https://staging.example.com', 'noindex')
->assertDispatched('configurationChanged')
->assertDispatched('success');
expect($this->application->refresh()->noindexDomains()->all())
->toBe(['https://staging.example.com']);
});
@@ -1,6 +1,6 @@
<?php
use App\Livewire\Project\Application\General;
use App\Livewire\Project\Application\Domains;
use App\Models\Application;
use App\Models\Environment;
use App\Models\InstanceSettings;
@@ -84,7 +84,7 @@ describe('Application noindex domains', function () {
expect($application->isDomainNoindexed('https://prod.example.com'))->toBeFalse();
});
test('the Livewire toggle persists the flag', function () {
test('the domains view toggle persists the flag', function () {
InstanceSettings::unguarded(function () {
InstanceSettings::updateOrCreate(['id' => 0], []);
});
@@ -107,10 +107,9 @@ describe('Application noindex domains', function () {
'redirect' => 'no',
]);
Livewire::test(General::class, ['application' => $application])
Livewire::test(Domains::class, ['application' => $application])
->assertSuccessful()
->set('noindexDomains', ['https://staging.example.com'])
->call('updateNoindexDomains')
->call('toggleNoindexDomain', 'https://staging.example.com', true)
->assertDispatched('success');
expect($application->refresh()->noindexDomains()->all())
@@ -159,6 +159,26 @@ it('shows domain changes when the domain page dispatches a configuration change'
->assertSee('https://changed.example.com');
});
it('shows noindex changes when the domains page dispatches a configuration change', function () {
$application = configurationCheckerApplication($this->environment, [
'fqdn' => 'https://example.com,https://staging.example.com',
]);
markConfigurationCheckerApplicationDeployed($application);
$component = Livewire::test(ConfigurationChecker::class, ['resource' => $application->refresh()])
->assertSet('isConfigurationChanged', false);
$application->setNoindexDomains(['https://staging.example.com']);
$application->save();
$component
->dispatch('configurationChanged')
->assertSet('isConfigurationChanged', true)
->assertSee('The latest configuration has not been applied')
->assertSee('Search engine indexing')
->assertSee('Redeploy to apply.');
});
it('shows an unapplied configuration warning after a directory mount is added', function () {
$application = configurationCheckerApplication($this->environment);
markConfigurationCheckerApplicationDeployed($application);
@@ -1,5 +1,6 @@
<?php
use App\Livewire\Project\Service\Domains;
use App\Livewire\Project\Service\EditDomain;
use App\Models\Environment;
use App\Models\Project;
@@ -77,9 +78,8 @@ it('loads the EditDomain component with required port', function () {
it('marks noindex changes as pending configuration', function () {
$this->service->isConfigurationChanged(save: true);
Livewire::test(EditDomain::class, ['applicationId' => $this->serviceApplication->id])
->set('noindexDomains', ['http://example.com:8000'])
->call('updateNoindexDomains')
Livewire::test(Domains::class, ['service' => $this->service->fresh(['applications', 'server'])])
->call('toggleNoindexDomain', $this->serviceApplication->id, 'http://example.com:8000', true)
->assertDispatched('configurationChanged');
expect($this->service->refresh()->isConfigurationChanged())->toBeTrue();
+41 -4
View File
@@ -90,7 +90,7 @@ beforeEach(function () {
]);
});
it('groups configured domains with their service redirect and excludes services without domains', function () {
it('groups configured domains and shows redirect settings in the edit modal', function () {
$this->apiApp->update([
'fqdn' => 'https://api.example.com,https://admin.example.com',
]);
@@ -100,18 +100,22 @@ it('groups configured domains with their service redirect and excludes services
->assertSee('API')
->assertSee('https://api.example.com')
->assertSee('https://admin.example.com')
->call('startEdit', 0)
->html();
expect($html)
->toContain("service-domain-group-{$this->apiApp->id}")
->toContain("id=\"service-domain-redirect-{$this->apiApp->id}-trigger\"")
->toContain("id=\"edit-service-domain-redirect-{$this->apiApp->id}-trigger\"")
->toContain("serviceRedirects.{$this->apiApp->id}")
->toContain('class="listbox-trigger"')
->toContain('application-settings-section-body is-flush mt-1 w-full scroll-mt-28 overflow-visible')
->not->toContain("<select id=\"service-domain-redirect-{$this->apiApp->id}\"")
->not->toContain("service-domain-redirect-toggle-{$this->apiApp->id}")
->toContain('dark:bg-white/[0.04]')
->toContain('<span>Domain</span>')
->toContain('<span>DNS</span>')
->toContain('<span>Last checked</span>')
->not->toContain("service-domain-group-{$this->webApp->id}")
->and(substr_count($html, '2 domains'))->toBe(1)
->and(strpos($html, '>API</span>'))->toBeLessThan(strpos($html, '<span>Domain</span>'))
->and(substr_count($html, "id=\"service-domain-group-{$this->apiApp->id}\""))->toBe(1);
});
@@ -194,6 +198,7 @@ it('saves the explicitly selected service redirect value', function () {
->call('updateServiceRedirect', $this->webApp->id, 'www')
->assertDispatched('success')
->assertSet('domainRows', fn (array $rows): bool => collect($rows)->pluck('url')->contains('https://www.web.example.com'))
->assertSet('domainRows', fn (array $rows): bool => filled(collect($rows)->firstWhere('url', 'https://www.web.example.com')['checked_at'] ?? null))
->assertSee('https://www.web.example.com');
expect($this->webApp->fresh()->redirect)->toBe('www');
@@ -270,6 +275,8 @@ it('provides client-side search for service domains', function () {
expect($view)
->toContain('x-model="domainSearch"')
->toContain('class="ml-auto flex flex-wrap items-center gap-2"')
->toContain('<div class="relative shrink-0">')
->toContain('placeholder="Search services or domains"')
->toContain('x-show="matchesDomainSearch(')
->toContain('title="No domains found"')
@@ -343,6 +350,8 @@ it('prunes the previous dns status when a service domain is renamed', function (
Livewire::test(Domains::class, ['service' => $this->service->fresh(['applications', 'server'])])
->call('startEdit', 0)
->assertSee('Direction')
->assertSee('Search engine indexing')
->set('editingDomain', 'https://renamed.example.com')
->call('updateDomain')
->assertHasNoErrors()
@@ -357,6 +366,15 @@ it('prunes the previous dns status when a service domain is renamed', function (
->and($this->apiApp->domain_dns_statuses['https://renamed.example.com']['status'])->toBe('skipped');
});
it('only shows the domain notification when redirect changes through edit domain', function () {
Livewire::test(Domains::class, ['service' => $this->service->fresh(['applications', 'server'])])
->call('startEdit', 0)
->set('editingDirection', 'www')
->call('updateDomain')
->assertDispatched('success', 'Domain updated.')
->assertNotDispatched('success', 'Redirect updated.');
});
it('does not restore stale dns status when a removed service domain is re-added', function () {
$this->apiApp->update([
'domain_dns_statuses' => [
@@ -509,3 +527,22 @@ it('exposes the stack domains route', function () {
->assertSeeLivewire(Domains::class)
->assertSee('Domains');
});
it('updates search engine indexing from the service domains view', function () {
Livewire::test(Domains::class, ['service' => $this->service->fresh(['applications', 'server'])])
->assertSee('Noindex')
->assertSee('Indexable')
->assertSee('x-model="localIndexing"', false)
->assertDontSee('@change="$wire.toggleNoindexDomain', false)
->assertDontSee('@change="$wire.updateServiceRedirect', false)
->assertDontSee('@js(', false)
->call('toggleNoindexDomain', $this->apiApp->id, 'https://api.example.com', 'noindex')
->assertDispatched('configurationChanged')
->assertDispatched('success');
expect($this->apiApp->refresh()->noindexDomains()->all())
->toBe(['https://api.example.com']);
expect(file_get_contents(resource_path('views/livewire/project/service/partials/domain-table.blade.php')))
->not->toContain('<select');
});