fix(env): keep dev view env saves independent of search
Search environment variables case-insensitively by key, add accessible search labeling, and ensure switching to developer view after searching loads the full variable set so non-matching entries are not removed on save.
This commit is contained in:
parent
ad4a21d15f
commit
d525c12457
3 changed files with 193 additions and 28 deletions
|
|
@ -7,6 +7,8 @@
|
||||||
use App\Support\ValidationPatterns;
|
use App\Support\ValidationPatterns;
|
||||||
use App\Traits\EnvironmentVariableProtection;
|
use App\Traits\EnvironmentVariableProtection;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class All extends Component
|
class All extends Component
|
||||||
|
|
@ -75,11 +77,24 @@ public function instantSave()
|
||||||
|
|
||||||
public function getEnvironmentVariablesProperty()
|
public function getEnvironmentVariablesProperty()
|
||||||
{
|
{
|
||||||
$query = $this->resource->environment_variables()
|
return $this->getEnvironmentVariables(false);
|
||||||
->orderByRaw("CASE WHEN is_required = true AND (value IS NULL OR value = '') THEN 0 ELSE 1 END");
|
}
|
||||||
|
|
||||||
if ($this->search !== '') {
|
public function getEnvironmentVariablesPreviewProperty()
|
||||||
$query->where('key', 'like', "%{$this->search}%");
|
{
|
||||||
|
return $this->getEnvironmentVariables(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getEnvironmentVariables(bool $isPreview, bool $withSearch = true): Collection
|
||||||
|
{
|
||||||
|
$query = $isPreview
|
||||||
|
? $this->resource->environment_variables_preview()
|
||||||
|
: $this->resource->environment_variables();
|
||||||
|
|
||||||
|
$query->orderByRaw("CASE WHEN is_required = true AND (value IS NULL OR value = '') THEN 0 ELSE 1 END");
|
||||||
|
|
||||||
|
if ($withSearch && $this->searchTerm() !== '') {
|
||||||
|
$query->whereRaw('LOWER(key) LIKE ?', ['%'.Str::lower($this->searchTerm()).'%']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->is_env_sorting_enabled) {
|
if ($this->is_env_sorting_enabled) {
|
||||||
|
|
@ -91,22 +106,9 @@ public function getEnvironmentVariablesProperty()
|
||||||
return $query->get();
|
return $query->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getEnvironmentVariablesPreviewProperty()
|
private function searchTerm(): string
|
||||||
{
|
{
|
||||||
$query = $this->resource->environment_variables_preview()
|
return trim($this->search);
|
||||||
->orderByRaw("CASE WHEN is_required = true AND (value IS NULL OR value = '') THEN 0 ELSE 1 END");
|
|
||||||
|
|
||||||
if ($this->search !== '') {
|
|
||||||
$query->where('key', 'like', "%{$this->search}%");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->is_env_sorting_enabled) {
|
|
||||||
$query->orderBy('key');
|
|
||||||
} else {
|
|
||||||
$query->orderBy('order');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $query->get();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getHardcodedEnvironmentVariablesProperty()
|
public function getHardcodedEnvironmentVariablesProperty()
|
||||||
|
|
@ -156,9 +158,9 @@ protected function getHardcodedVariables(bool $isPreview)
|
||||||
return ! in_array($var['key'], $managedKeys);
|
return ! in_array($var['key'], $managedKeys);
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($this->search !== '') {
|
if ($this->searchTerm() !== '') {
|
||||||
$hardcodedVars = $hardcodedVars->filter(function ($var) {
|
$hardcodedVars = $hardcodedVars->filter(function ($var) {
|
||||||
return str($var['key'])->contains($this->search, true);
|
return str($var['key'])->contains($this->searchTerm(), true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,9 +175,9 @@ protected function getHardcodedVariables(bool $isPreview)
|
||||||
|
|
||||||
public function getDevView()
|
public function getDevView()
|
||||||
{
|
{
|
||||||
$this->variables = $this->formatEnvironmentVariables($this->environmentVariables);
|
$this->variables = $this->formatEnvironmentVariables($this->getEnvironmentVariables(false, false));
|
||||||
if ($this->showPreview) {
|
if ($this->showPreview) {
|
||||||
$this->variablesPreview = $this->formatEnvironmentVariables($this->environmentVariablesPreview);
|
$this->variablesPreview = $this->formatEnvironmentVariables($this->getEnvironmentVariables(true, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,17 +50,17 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full md:w-96">
|
<div class="w-full md:w-96">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input type="search" placeholder="Search" wire:model.live.debounce.300ms="search"
|
<input type="search" placeholder="Search" aria-label="Search environment variables"
|
||||||
class="w-full input pl-10" />
|
wire:model.live.debounce.300ms="search" class="w-full input pl-10" />
|
||||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||||
<div class="relative w-4 h-4">
|
<div class="relative w-4 h-4">
|
||||||
<svg wire:loading.remove wire:target="search"
|
<svg wire:loading.remove wire:target="search" aria-hidden="true"
|
||||||
class="absolute inset-0 w-4 h-4 dark:text-neutral-400" fill="none"
|
class="absolute inset-0 w-4 h-4 dark:text-neutral-400" fill="none"
|
||||||
stroke="currentColor" viewBox="0 0 24 24">
|
stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
<svg wire:loading wire:target="search"
|
<svg wire:loading wire:target="search" aria-hidden="true"
|
||||||
class="absolute inset-0 w-4 h-4 text-coollabs dark:text-warning animate-spin"
|
class="absolute inset-0 w-4 h-4 text-coollabs dark:text-warning animate-spin"
|
||||||
fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
|
||||||
|
|
@ -131,4 +131,4 @@ class="absolute inset-0 w-4 h-4 text-coollabs dark:text-warning animate-spin"
|
||||||
@endcan
|
@endcan
|
||||||
</form>
|
</form>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
163
tests/Feature/EnvironmentVariableSearchTest.php
Normal file
163
tests/Feature/EnvironmentVariableSearchTest.php
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Livewire\Project\Shared\EnvironmentVariable\All;
|
||||||
|
use App\Models\Application;
|
||||||
|
use App\Models\Environment;
|
||||||
|
use App\Models\EnvironmentVariable;
|
||||||
|
use App\Models\InstanceSettings;
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\Service;
|
||||||
|
use App\Models\Team;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
InstanceSettings::forceCreate(['id' => 0]);
|
||||||
|
|
||||||
|
$this->user = User::factory()->create();
|
||||||
|
$this->team = Team::factory()->create();
|
||||||
|
$this->team->members()->attach($this->user, ['role' => 'owner']);
|
||||||
|
$this->project = Project::factory()->create([
|
||||||
|
'team_id' => $this->team->id,
|
||||||
|
]);
|
||||||
|
$this->environment = Environment::factory()->create([
|
||||||
|
'project_id' => $this->project->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters production environment variables by key case-insensitively', function () {
|
||||||
|
$application = Application::factory()->create([
|
||||||
|
'environment_id' => $this->environment->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
EnvironmentVariable::create([
|
||||||
|
'key' => 'API_KEY',
|
||||||
|
'value' => 'secret',
|
||||||
|
'resourceable_type' => Application::class,
|
||||||
|
'resourceable_id' => $application->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
EnvironmentVariable::create([
|
||||||
|
'key' => 'DATABASE_URL',
|
||||||
|
'value' => 'postgres://example',
|
||||||
|
'resourceable_type' => Application::class,
|
||||||
|
'resourceable_id' => $application->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$component = Livewire::test(All::class, ['resource' => $application])
|
||||||
|
->set('search', 'api');
|
||||||
|
|
||||||
|
expect($component->instance()->environmentVariables->pluck('key')->all())
|
||||||
|
->toBe(['API_KEY']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters preview environment variables by key case-insensitively', function () {
|
||||||
|
$application = Application::factory()->create([
|
||||||
|
'environment_id' => $this->environment->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
EnvironmentVariable::create([
|
||||||
|
'key' => 'PREVIEW_TOKEN',
|
||||||
|
'value' => 'preview-secret',
|
||||||
|
'is_preview' => true,
|
||||||
|
'resourceable_type' => Application::class,
|
||||||
|
'resourceable_id' => $application->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
EnvironmentVariable::create([
|
||||||
|
'key' => 'OTHER_PREVIEW_VALUE',
|
||||||
|
'value' => 'preview-other',
|
||||||
|
'is_preview' => true,
|
||||||
|
'resourceable_type' => Application::class,
|
||||||
|
'resourceable_id' => $application->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$component = Livewire::test(All::class, ['resource' => $application])
|
||||||
|
->set('search', 'token');
|
||||||
|
|
||||||
|
expect($component->instance()->environmentVariablesPreview->pluck('key')->all())
|
||||||
|
->toBe(['PREVIEW_TOKEN']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters hardcoded Docker Compose environment variables by key case-insensitively', function () {
|
||||||
|
$service = Service::factory()->create([
|
||||||
|
'environment_id' => $this->environment->id,
|
||||||
|
'docker_compose_raw' => <<<'YAML'
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: nginx
|
||||||
|
environment:
|
||||||
|
API_TOKEN: hardcoded-secret
|
||||||
|
DATABASE_URL: postgres://example
|
||||||
|
YAML,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$component = Livewire::test(All::class, ['resource' => $service])
|
||||||
|
->set('search', 'api');
|
||||||
|
|
||||||
|
expect($component->instance()->hardcodedEnvironmentVariables->pluck('key')->all())
|
||||||
|
->toBe(['API_TOKEN']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps developer view unfiltered after searching', function () {
|
||||||
|
$application = Application::factory()->create([
|
||||||
|
'environment_id' => $this->environment->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
EnvironmentVariable::create([
|
||||||
|
'key' => 'API_KEY',
|
||||||
|
'value' => 'secret',
|
||||||
|
'resourceable_type' => Application::class,
|
||||||
|
'resourceable_id' => $application->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
EnvironmentVariable::create([
|
||||||
|
'key' => 'DATABASE_URL',
|
||||||
|
'value' => 'postgres://example',
|
||||||
|
'resourceable_type' => Application::class,
|
||||||
|
'resourceable_id' => $application->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$component = Livewire::test(All::class, ['resource' => $application])
|
||||||
|
->set('search', 'api')
|
||||||
|
->call('switch')
|
||||||
|
->assertSet('view', 'dev');
|
||||||
|
|
||||||
|
expect($component->get('variables'))
|
||||||
|
->toContain('API_KEY=secret')
|
||||||
|
->toContain('DATABASE_URL=postgres://example');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not delete non-matching variables when saving developer view after searching', function () {
|
||||||
|
$application = Application::factory()->create([
|
||||||
|
'environment_id' => $this->environment->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
EnvironmentVariable::create([
|
||||||
|
'key' => 'API_KEY',
|
||||||
|
'value' => 'secret',
|
||||||
|
'resourceable_type' => Application::class,
|
||||||
|
'resourceable_id' => $application->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
EnvironmentVariable::create([
|
||||||
|
'key' => 'DATABASE_URL',
|
||||||
|
'value' => 'postgres://example',
|
||||||
|
'resourceable_type' => Application::class,
|
||||||
|
'resourceable_id' => $application->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(All::class, ['resource' => $application])
|
||||||
|
->set('search', 'api')
|
||||||
|
->call('switch')
|
||||||
|
->call('submit');
|
||||||
|
|
||||||
|
expect($application->environment_variables()->pluck('key')->all())
|
||||||
|
->toContain('API_KEY')
|
||||||
|
->toContain('DATABASE_URL');
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue