mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 21:18:11 -04:00
feat: show the full file path inline and warn on a repeated base dir (#12081)
This commit is contained in:
@@ -460,7 +460,7 @@ class General extends Component
|
||||
$this->portsMappings = $this->application->ports_mappings;
|
||||
$this->customNetworkAliases = $this->application->custom_network_aliases;
|
||||
$this->dockerfile = $this->application->dockerfile;
|
||||
$this->dockerfileLocation = $this->application->dockerfile_location;
|
||||
$this->dockerfileLocation = $this->application->dockerfile_location ?? ($this->buildPack === 'dockerfile' ? '/Dockerfile' : null);
|
||||
$this->dockerfileTargetBuild = $this->application->dockerfile_target_build;
|
||||
$this->dockerRegistryImageName = $this->application->docker_registry_image_name;
|
||||
$this->dockerRegistryImageTag = $this->application->docker_registry_image_tag;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
@props([
|
||||
'base',
|
||||
'file' => null,
|
||||
'fileLabel' => null,
|
||||
'defaultFile' => null,
|
||||
'disabled' => 'false',
|
||||
])
|
||||
|
||||
<div wire:key="repository-paths-{{ $file }}" {{ $attributes->class('grid gap-4') }} x-data="{
|
||||
base: $wire.entangle('{{ $base }}'),
|
||||
file: {{ $file ? "\$wire.entangle('{$file}')" : 'null' }},
|
||||
normalize(path) {
|
||||
path = (path ?? '').trim().replace(/\/+$/, '');
|
||||
return path === '' || path.startsWith('/') ? path : '/' + path;
|
||||
},
|
||||
get directory() {
|
||||
return this.normalize(this.base);
|
||||
},
|
||||
get location() {
|
||||
return this.normalize(this.file) || '{{ $defaultFile }}';
|
||||
},
|
||||
get repeatsBase() {
|
||||
return this.directory !== '' && this.location.startsWith(this.directory + '/');
|
||||
},
|
||||
get suggestion() {
|
||||
return this.location.slice(this.directory.length);
|
||||
},
|
||||
}">
|
||||
<x-forms.input label="Base directory" placeholder="/"
|
||||
helper="Repository directory used as the build root. Useful for monorepos." x-model="base"
|
||||
x-bind:disabled="{{ $disabled }}" @blur="base = directory || '/'" />
|
||||
@if ($file)
|
||||
<x-forms.input :label="$fileLabel" :placeholder="$defaultFile" helper="Path relative to the base directory."
|
||||
x-model="file" x-bind:disabled="{{ $disabled }}" @blur="file = location" />
|
||||
<p class="col-span-full text-xs text-neutral-500 dark:text-fg-dim">
|
||||
Resolved file:
|
||||
<code class="font-mono text-coollabs dark:text-warning" x-text="directory + location"></code>
|
||||
</p>
|
||||
<x-callout class="col-span-full" x-cloak x-show="repeatsBase" title="Base directory is repeated">
|
||||
{{ $fileLabel }} is relative to the base directory, so it should not start with
|
||||
<code class="rounded bg-black/10 px-1 dark:bg-white/10" x-text="directory"></code> again.
|
||||
<button type="button" class="font-medium underline" x-show="!({{ $disabled }})"
|
||||
@click="file = suggestion">Use <span x-text="suggestion"></span></button>
|
||||
</x-callout>
|
||||
@endif
|
||||
{{ $slot }}
|
||||
</div>
|
||||
@@ -144,35 +144,9 @@
|
||||
<div class="flex flex-col gap-5">
|
||||
@if ($buildPack === 'dockercompose')
|
||||
<div class="flex flex-col gap-2">
|
||||
<div x-data="{
|
||||
baseDir: @entangle('baseDirectory'),
|
||||
composeLocation: @entangle('dockerComposeLocation'),
|
||||
normalizePath(path) {
|
||||
if (!path || path.trim() === '') return '/';
|
||||
path = path.trim();
|
||||
path = path.replace(/\/+$/, '');
|
||||
if (!path.startsWith('/')) {
|
||||
path = '/' + path;
|
||||
}
|
||||
return path;
|
||||
},
|
||||
normalizeBaseDir() {
|
||||
this.baseDir = this.normalizePath(this.baseDir);
|
||||
},
|
||||
normalizeComposeLocation() {
|
||||
this.composeLocation = this.normalizePath(this.composeLocation);
|
||||
}
|
||||
}" class="grid gap-4 lg:grid-cols-2">
|
||||
<x-forms.input x-bind:disabled="shouldDisable()" placeholder="/"
|
||||
label="Base directory"
|
||||
helper="Directory to use as root. Useful for monorepos." x-model="baseDir"
|
||||
@blur="normalizeBaseDir()" />
|
||||
<x-forms.input x-bind:disabled="shouldDisable()"
|
||||
placeholder="/docker-compose.yaml"
|
||||
label="Docker compose location"
|
||||
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($baseDirectory . $dockerComposeLocation, '/') }}</span>"
|
||||
x-model="composeLocation" @blur="normalizeComposeLocation()" />
|
||||
</div>
|
||||
<x-forms.repository-paths class="lg:grid-cols-2" base="baseDirectory"
|
||||
file="dockerComposeLocation" fileLabel="Docker compose location"
|
||||
defaultFile="/docker-compose.yaml" disabled="shouldDisable()" />
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.checkbox instantSave id="isPreserveRepositoryEnabled"
|
||||
label="Preserve repository during deployment"
|
||||
@@ -215,41 +189,9 @@
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<div x-data="{
|
||||
baseDir: @entangle('baseDirectory'),
|
||||
dockerfileLocation: @entangle('dockerfileLocation'),
|
||||
normalizePath(path) {
|
||||
if (!path || path.trim() === '') return '/';
|
||||
path = path.trim();
|
||||
path = path.replace(/\/+$/, '');
|
||||
if (!path.startsWith('/')) {
|
||||
path = '/' + path;
|
||||
}
|
||||
return path;
|
||||
},
|
||||
normalizeBaseDir() {
|
||||
this.baseDir = this.normalizePath(this.baseDir);
|
||||
},
|
||||
normalizeDockerfileLocation() {
|
||||
this.dockerfileLocation = this.normalizePath(this.dockerfileLocation);
|
||||
}
|
||||
}" class="grid gap-4 lg:grid-cols-2">
|
||||
<x-forms.input placeholder="/"
|
||||
label="Base directory" helper="Directory to use as root. Useful for monorepos."
|
||||
x-bind:disabled="!canUpdate" x-model="baseDir" @blur="normalizeBaseDir()" />
|
||||
@if ($buildPack === 'dockerfile' && !$application->dockerfile)
|
||||
<x-forms.input placeholder="/Dockerfile"
|
||||
label="Dockerfile location"
|
||||
helper="It is calculated together with the Base Directory:<br><span class='dark:text-warning'>{{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}</span>"
|
||||
x-bind:disabled="!canUpdate" x-model="dockerfileLocation"
|
||||
@blur="normalizeDockerfileLocation()" />
|
||||
@endif
|
||||
|
||||
@if ($buildPack === 'dockerfile')
|
||||
<x-forms.input id="dockerfileTargetBuild" label="Docker build stage target"
|
||||
helper="Useful if you have multi-staged dockerfile."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
<x-forms.repository-paths class="lg:grid-cols-2" base="baseDirectory"
|
||||
:file="$buildPack === 'dockerfile' && !$application->dockerfile ? 'dockerfileLocation' : null"
|
||||
fileLabel="Dockerfile location" defaultFile="/Dockerfile" disabled="!canUpdate">
|
||||
@if ($application->could_set_build_commands())
|
||||
@if ($application->settings->is_static)
|
||||
<x-forms.input placeholder="/dist" id="publishDirectory"
|
||||
@@ -259,8 +201,7 @@
|
||||
label="Publish directory" x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</x-forms.repository-paths>
|
||||
@if ($this->application->is_github_based() && !$this->application->is_public_repository())
|
||||
<div class="pb-4">
|
||||
<x-forms.textarea
|
||||
@@ -293,6 +234,11 @@
|
||||
: 'If no usable build server is available, the deployment fails.';
|
||||
@endphp
|
||||
<div class="grid gap-4 pt-2 sm:grid-cols-2">
|
||||
@if ($buildPack === 'dockerfile')
|
||||
<x-forms.input id="dockerfileTargetBuild" label="Docker build stage target"
|
||||
helper="Useful if you have multi-staged dockerfile."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@endif
|
||||
<x-forms.listbox id="isBuildServerEnabled" label="Builder selection"
|
||||
onChange="instantSave" :options="$buildServerOptions"
|
||||
helper="Build your application on a dedicated build server. If several build servers are connected, Coolify picks an available one automatically. {{ $buildServerFallbackPolicy }} More info in the <a href='https://coolify.io/docs/knowledge-base/server/build-server' class='underline' target='_blank'>documentation</a>."
|
||||
|
||||
+3
-22
@@ -77,28 +77,9 @@
|
||||
</div>
|
||||
|
||||
@if ($build_pack === 'dockercompose')
|
||||
<div x-data="{
|
||||
baseDir: @js($base_directory),
|
||||
composeLocation: @js($docker_compose_location),
|
||||
normalize(path) {
|
||||
if (!path || path.trim() === '') return '/';
|
||||
const normalized = path.trim().replace(/\/+$/, '');
|
||||
return normalized.startsWith('/') ? normalized : '/' + normalized;
|
||||
},
|
||||
}" class="grid gap-4 sm:grid-cols-2">
|
||||
<x-forms.input placeholder="/" wire:model.defer="base_directory"
|
||||
label="Base directory" helper="Repository directory used as the build root."
|
||||
x-model="baseDir" @blur="baseDir = normalize(baseDir)" />
|
||||
<x-forms.input placeholder="/docker-compose.yaml"
|
||||
wire:model.defer="docker_compose_location" label="Compose file"
|
||||
helper="Path relative to the base directory." x-model="composeLocation"
|
||||
@blur="composeLocation = normalize(composeLocation)" />
|
||||
<p class="sm:col-span-2 text-xs text-neutral-500 dark:text-fg-dim">
|
||||
Resolved file:
|
||||
<code class="font-mono text-coollabs dark:text-warning"
|
||||
x-text='(baseDir === "/" ? "" : baseDir) + (composeLocation.startsWith("/") ? composeLocation : "/" + composeLocation)'></code>
|
||||
</p>
|
||||
</div>
|
||||
<x-forms.repository-paths class="sm:grid-cols-2" base="base_directory"
|
||||
file="docker_compose_location" fileLabel="Compose file"
|
||||
defaultFile="/docker-compose.yaml" />
|
||||
@else
|
||||
<x-forms.input wire:model="base_directory" label="Base directory"
|
||||
helper="Repository directory used as the build root." />
|
||||
|
||||
@@ -154,28 +154,9 @@
|
||||
@endif
|
||||
|
||||
@if ($build_pack === 'dockercompose')
|
||||
<div x-data="{
|
||||
baseDir: @js($base_directory),
|
||||
composeLocation: @js($docker_compose_location),
|
||||
normalize(path) {
|
||||
if (!path || path.trim() === '') return '/';
|
||||
const normalized = path.trim().replace(/\/+$/, '');
|
||||
return normalized.startsWith('/') ? normalized : '/' + normalized;
|
||||
},
|
||||
}" class="grid gap-4 sm:grid-cols-2">
|
||||
<x-forms.input placeholder="/" wire:model.defer="base_directory"
|
||||
label="Base directory" helper="Repository directory used as the build root."
|
||||
x-model="baseDir" @blur="baseDir = normalize(baseDir)" />
|
||||
<x-forms.input placeholder="/docker-compose.yaml"
|
||||
wire:model.defer="docker_compose_location" label="Compose file"
|
||||
helper="Path relative to the base directory." x-model="composeLocation"
|
||||
@blur="composeLocation = normalize(composeLocation)" />
|
||||
<p class="sm:col-span-2 text-xs text-neutral-500 dark:text-fg-dim">
|
||||
Resolved file:
|
||||
<code class="font-mono text-coollabs dark:text-warning"
|
||||
x-text='(baseDir === "/" ? "" : baseDir) + (composeLocation.startsWith("/") ? composeLocation : "/" + composeLocation)'></code>
|
||||
</p>
|
||||
</div>
|
||||
<x-forms.repository-paths class="sm:grid-cols-2" base="base_directory"
|
||||
file="docker_compose_location" fileLabel="Compose file"
|
||||
defaultFile="/docker-compose.yaml" />
|
||||
@else
|
||||
<x-forms.input wire:model="base_directory" label="Base directory"
|
||||
helper="Repository directory used as the build root." />
|
||||
|
||||
@@ -131,28 +131,9 @@
|
||||
</div>
|
||||
|
||||
@if ($build_pack === 'dockercompose')
|
||||
<div x-data="{
|
||||
baseDir: @js($base_directory),
|
||||
composeLocation: @js($docker_compose_location),
|
||||
normalize(path) {
|
||||
if (!path || path.trim() === '') return '/';
|
||||
const normalized = path.trim().replace(/\/+$/, '');
|
||||
return normalized.startsWith('/') ? normalized : '/' + normalized;
|
||||
},
|
||||
}" class="grid gap-4 sm:grid-cols-2">
|
||||
<x-forms.input placeholder="/" wire:model.defer="base_directory"
|
||||
label="Base directory" helper="Repository directory used as the build root."
|
||||
x-model="baseDir" @blur="baseDir = normalize(baseDir)" />
|
||||
<x-forms.input placeholder="/docker-compose.yaml"
|
||||
wire:model.defer="docker_compose_location" label="Compose file"
|
||||
helper="Path relative to the base directory." x-model="composeLocation"
|
||||
@blur="composeLocation = normalize(composeLocation)" />
|
||||
<p class="sm:col-span-2 text-xs text-neutral-500 dark:text-fg-dim">
|
||||
Resolved file:
|
||||
<code class="font-mono text-coollabs dark:text-warning"
|
||||
x-text='(baseDir === "/" ? "" : baseDir) + (composeLocation.startsWith("/") ? composeLocation : "/" + composeLocation)'></code>
|
||||
</p>
|
||||
</div>
|
||||
<x-forms.repository-paths class="sm:grid-cols-2" base="base_directory"
|
||||
file="docker_compose_location" fileLabel="Compose file"
|
||||
defaultFile="/docker-compose.yaml" />
|
||||
@else
|
||||
<x-forms.input wire:model="base_directory" label="Base directory"
|
||||
helper="Repository directory used as the build root." />
|
||||
|
||||
@@ -103,28 +103,9 @@
|
||||
@endif
|
||||
|
||||
@if ($build_pack === 'dockercompose')
|
||||
<div x-data="{
|
||||
baseDir: @js($base_directory),
|
||||
composeLocation: @js($docker_compose_location),
|
||||
normalize(path) {
|
||||
if (!path || path.trim() === '') return '/';
|
||||
const normalized = path.trim().replace(/\/+$/, '');
|
||||
return normalized.startsWith('/') ? normalized : '/' + normalized;
|
||||
},
|
||||
}" class="grid gap-4 sm:grid-cols-2">
|
||||
<x-forms.input placeholder="/" wire:model.defer="base_directory"
|
||||
label="Base directory" helper="Repository directory used as the build root."
|
||||
x-model="baseDir" @blur="baseDir = normalize(baseDir)" />
|
||||
<x-forms.input placeholder="/docker-compose.yaml"
|
||||
wire:model.defer="docker_compose_location" label="Compose file"
|
||||
helper="Path relative to the base directory." x-model="composeLocation"
|
||||
@blur="composeLocation = normalize(composeLocation)" />
|
||||
<p class="sm:col-span-2 text-xs text-neutral-500 dark:text-fg-dim">
|
||||
Resolved file:
|
||||
<code class="font-mono text-coollabs dark:text-warning"
|
||||
x-text='(baseDir === "/" ? "" : baseDir) + (composeLocation.startsWith("/") ? composeLocation : "/" + composeLocation)'></code>
|
||||
</p>
|
||||
</div>
|
||||
<x-forms.repository-paths class="sm:grid-cols-2" base="base_directory"
|
||||
file="docker_compose_location" fileLabel="Compose file"
|
||||
defaultFile="/docker-compose.yaml" />
|
||||
@else
|
||||
<x-forms.input wire:model="base_directory" label="Base directory"
|
||||
helper="Repository directory used as the build root." />
|
||||
|
||||
@@ -122,6 +122,26 @@ test('switching from railpack to compose preserves the existing application doma
|
||||
->toBe('https://example.com,https://www.example.com');
|
||||
});
|
||||
|
||||
test('Dockerfile location defaults to the Dockerfile only for the Dockerfile build pack', function (string $buildPack, ?string $expected) {
|
||||
$application = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => StandaloneDocker::class,
|
||||
'build_pack' => $buildPack,
|
||||
'static_image' => 'nginx:alpine',
|
||||
'base_directory' => '/',
|
||||
'is_http_basic_auth_enabled' => false,
|
||||
'redirect' => 'no',
|
||||
]);
|
||||
|
||||
Livewire::test(General::class, ['application' => $application])
|
||||
->assertSuccessful()
|
||||
->assertSet('dockerfileLocation', $expected);
|
||||
})->with([
|
||||
'Dockerfile' => ['dockerfile', '/Dockerfile'],
|
||||
'Nixpacks' => ['nixpacks', null],
|
||||
]);
|
||||
|
||||
test('networking section hints that internal ports can be set per domain', function () {
|
||||
$application = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
|
||||
@@ -156,6 +156,59 @@ it('saves custom docker run options from the UI', function () {
|
||||
->screenshot(filename: 'application-docker-run-options');
|
||||
});
|
||||
|
||||
it('warns when the Dockerfile location repeats the base directory and applies the suggested fix', function () {
|
||||
$this->application->update([
|
||||
'build_pack' => 'dockerfile',
|
||||
'base_directory' => '/apps/api',
|
||||
'dockerfile_location' => '/apps/api/Dockerfile',
|
||||
]);
|
||||
|
||||
loginAndSkipBoarding();
|
||||
|
||||
$page = visit(applicationConfigurationUrl(
|
||||
$this->stack['project'],
|
||||
$this->stack['environment'],
|
||||
$this->application
|
||||
));
|
||||
|
||||
$page->assertSee('Base directory is repeated')
|
||||
->assertSee('/apps/api/apps/api/Dockerfile')
|
||||
->screenshot(filename: 'application-dockerfile-location-repeats-base-directory')
|
||||
->click('Use /Dockerfile')
|
||||
->assertDontSee('Base directory is repeated')
|
||||
->assertSee('/apps/api/Dockerfile');
|
||||
|
||||
submitLivewireForm($page);
|
||||
|
||||
$page->assertNoJavaScriptErrors()
|
||||
->screenshot(filename: 'application-dockerfile-location-fixed');
|
||||
|
||||
$this->application->refresh();
|
||||
expect($this->application->base_directory)->toBe('/apps/api')
|
||||
->and($this->application->dockerfile_location)->toBe('/Dockerfile');
|
||||
});
|
||||
|
||||
it('restores the default file location when the field is cleared', function (string $buildPack, string $column, string $default) {
|
||||
$this->application->update(['build_pack' => $buildPack, $column => '/docker/custom']);
|
||||
|
||||
loginAndSkipBoarding();
|
||||
|
||||
$page = visit(applicationConfigurationUrl(
|
||||
$this->stack['project'],
|
||||
$this->stack['environment'],
|
||||
$this->application
|
||||
));
|
||||
|
||||
$page->clear("input[placeholder='{$default}']")
|
||||
->keys("input[placeholder='{$default}']", 'Tab')
|
||||
->assertValue("input[placeholder='{$default}']", $default)
|
||||
->assertNoJavaScriptErrors()
|
||||
->screenshot(filename: "application-{$buildPack}-location-cleared");
|
||||
})->with([
|
||||
'Dockerfile' => ['dockerfile', 'dockerfile_location', '/Dockerfile'],
|
||||
'Docker Compose' => ['dockercompose', 'docker_compose_location', '/docker-compose.yaml'],
|
||||
]);
|
||||
|
||||
it('opens environment variables page for the application', function () {
|
||||
loginAndSkipBoarding();
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('restores the default compose file when the field is cleared', function () {
|
||||
$stack = seedBrowserResourceStack();
|
||||
loginAndSkipBoarding();
|
||||
|
||||
$page = visit("/project/{$stack['project']->uuid}/environment/{$stack['environment']->uuid}/new?type=public&destination={$stack['destination']->uuid}");
|
||||
|
||||
$page->fill('repository_url', 'https://gitlab.com/example/monorepo.git')
|
||||
->click('Check repository')
|
||||
->assertSee('Build configuration');
|
||||
|
||||
selectListboxOption($page, 'build_pack', 'Docker Compose');
|
||||
|
||||
$page->clear("input[placeholder='/docker-compose.yaml']")
|
||||
->keys("input[placeholder='/docker-compose.yaml']", 'Tab')
|
||||
->assertValue("input[placeholder='/docker-compose.yaml']", '/docker-compose.yaml')
|
||||
->assertNoJavaScriptErrors()
|
||||
->screenshot(filename: 'new-application-compose-file-cleared');
|
||||
});
|
||||
|
||||
it('warns when the compose file repeats the base directory and creates the application with the suggested fix', function () {
|
||||
$stack = seedBrowserResourceStack();
|
||||
loginAndSkipBoarding();
|
||||
|
||||
$page = visit("/project/{$stack['project']->uuid}/environment/{$stack['environment']->uuid}/new?type=public&destination={$stack['destination']->uuid}");
|
||||
|
||||
$page->fill('repository_url', 'https://gitlab.com/example/monorepo.git')
|
||||
->click('Check repository')
|
||||
->assertSee('Build configuration');
|
||||
|
||||
selectListboxOption($page, 'build_pack', 'Docker Compose');
|
||||
|
||||
$page->fill("input[placeholder='/']", 'apps/api/')
|
||||
->fill("input[placeholder='/docker-compose.yaml']", 'apps/api/compose.yaml')
|
||||
->assertSee('Base directory is repeated')
|
||||
->assertSee('/apps/api/apps/api/compose.yaml')
|
||||
->screenshot(filename: 'new-application-compose-file-repeats-base-directory')
|
||||
->click('button:has-text("Use /compose.yaml")')
|
||||
->assertDontSee('Base directory is repeated')
|
||||
->assertSee('/apps/api/compose.yaml')
|
||||
->assertNoJavaScriptErrors()
|
||||
->screenshot(filename: 'new-application-compose-file-fixed');
|
||||
|
||||
submitLivewireForm($page);
|
||||
|
||||
$application = Application::query()->where('git_repository', 'https://gitlab.com/example/monorepo.git')->sole();
|
||||
expect($application->base_directory)->toBe('/apps/api')
|
||||
->and($application->docker_compose_location)->toBe('/compose.yaml');
|
||||
});
|
||||
Reference in New Issue
Block a user