refactor(database): move flush cache from action menu to danger page

Relocate the cache flush action out of the Heading dropdown into a
dedicated FlushCache section on the database /danger page, shown only
for Redis, KeyDB and Dragonfly. Keeps the shared Danger component free
of cache-specific branches. Tests updated to target the new component.
This commit is contained in:
Aditya Tripathi
2026-10-04 22:57:48 +02:00
committed by peaklabs-dev
parent e67b27040f
commit 4429945da6
6 changed files with 142 additions and 87 deletions
@@ -0,0 +1,61 @@
<?php
namespace App\Livewire\Project\Database;
use App\Actions\Database\FlushCacheDatabase;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneRedis;
use Illuminate\Contracts\View\View;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class FlushCache extends Component
{
use AuthorizesRequests;
public $database;
public function mount(): void
{
$this->authorize('view', $this->database);
}
public function flush(): void
{
try {
$this->authorize('manage', $this->database);
if (! $this->isCacheDatabase()) {
throw new \RuntimeException('This database type does not support cache flushing.');
}
FlushCacheDatabase::run($this->database);
$this->auditFlush();
$this->dispatch('success', 'Cache flushed successfully.');
} catch (\Throwable $e) {
handleError($e, $this);
}
}
public function isCacheDatabase(): bool
{
return $this->database instanceof StandaloneRedis
|| $this->database instanceof StandaloneKeydb
|| $this->database instanceof StandaloneDragonfly;
}
private function auditFlush(): void
{
auditLog('ui.database.flushed', [
'team_id' => $this->database->team()?->id,
'database_uuid' => $this->database->uuid,
'database_name' => $this->database->name,
]);
}
public function render(): View
{
return view('livewire.project.database.flush-cache');
}
}
-29
View File
@@ -2,15 +2,11 @@
namespace App\Livewire\Project\Database;
use App\Actions\Database\FlushCacheDatabase;
use App\Actions\Database\RestartDatabase;
use App\Actions\Database\StartDatabase;
use App\Actions\Database\StopDatabase;
use App\Actions\Docker\GetContainersStatus;
use App\Events\ServiceStatusChanged;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneRedis;
use App\Support\ResourceStartActivity;
use App\Traits\ListensToTeamChannel;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
@@ -150,23 +146,6 @@ class Heading extends Component
}
}
public function flush()
{
try {
$this->authorize('manage', $this->database);
if (! $this->isCacheDatabase()) {
throw new \RuntimeException('This database type does not support cache flushing.');
}
FlushCacheDatabase::run($this->database);
$this->auditDatabaseAction('ui.database.flushed');
$this->dispatch('success', 'Cache flushed successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function restart()
{
try {
@@ -213,17 +192,9 @@ class Heading extends Component
'checkboxes' => [
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
],
'isCacheDatabase' => $this->isCacheDatabase(),
]);
}
private function isCacheDatabase(): bool
{
return $this->database instanceof StandaloneRedis
|| $this->database instanceof StandaloneKeydb
|| $this->database instanceof StandaloneDragonfly;
}
private function auditDatabaseAction(string $event): void
{
auditLog($event, [
@@ -53,6 +53,9 @@
@elseif ($currentRoute === 'project.database.tags')
<livewire:project.shared.tags :resource="$database" />
@elseif ($currentRoute === 'project.database.danger')
@if (in_array($database->type(), ['standalone-redis', 'standalone-keydb', 'standalone-dragonfly']))
<livewire:project.database.flush-cache :database="$database" />
@endif
<livewire:project.shared.danger :resource="$database" />
@endif
</div>
@@ -0,0 +1,43 @@
<div>
<x-application.settings-section id="flush-cache-section" title="Flush cache"
helper="Remove every key stored in this cache database.">
<div
class="rounded-lg border border-amber-300 bg-amber-50 p-4 ring-1 ring-inset ring-amber-200/60 dark:border-warning/30 dark:bg-warning/[0.08] dark:ring-warning/10">
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-2">
<h4 class="text-sm font-semibold text-amber-700 dark:text-warning">Flush cache</h4>
<x-status-badge status="Irreversible" type="warning" />
</div>
<p class="mt-2 max-w-2xl text-[13px] leading-5 text-neutral-600 dark:text-fg-dim">
Permanently erase every key in
<strong class="font-semibold text-black dark:text-fg">{{ $database->name }}</strong>
by running <code>FLUSHALL ASYNC</code> inside the running container.
</p>
<ul class="mt-3 space-y-1 text-xs text-neutral-500 dark:text-fg-dim">
<li>• All cached data is removed and cannot be recovered.</li>
<li>• The database container keeps running; only its data is cleared.</li>
<li>• The database must be running for this action to work.</li>
</ul>
</div>
<div class="shrink-0">
@can('manage', $database)
<x-modal-confirmation title="Flush cache?" buttonTitle="Flush cache" submitAction="flush"
:confirmWithText="true" confirmationText="{{ $database->name }}"
confirmationLabel="Enter the database name to confirm flushing the cache"
shortConfirmationLabel="Database name" :confirmWithPassword="false"
:actions="[
'Every key stored in this cache will be permanently erased.',
'This action cannot be undone and data cannot be recovered.',
]" step1ButtonText="Continue" step2ButtonText="Flush cache" />
@else
<x-forms.button disabled tooltip="You do not have permission to flush this cache.">
Flush cache
</x-forms.button>
@endcan
</div>
</div>
</div>
</x-application.settings-section>
</div>
@@ -88,13 +88,6 @@
<x-reicon name="restart" class="size-3.5" x-show="!busy" />
<span x-text="busy ? 'Restarting…' : 'Restart'">Restart</span>
</x-slot:main>
@if ($isCacheDatabase)
<button type="button" class="listbox-option justify-start! gap-2.5!"
@click="open = false; document.getElementById('database-flush-trigger')?.click()" role="menuitem">
<x-reicon name="trash" class="size-3.5 text-error" />
Flush cache
</button>
@endif
<button type="button" class="listbox-option justify-start! gap-2.5!"
@click="open = false; document.getElementById('database-stop-trigger')?.click()" role="menuitem">
<x-reicon name="stop-circle" class="size-3.5 text-error" />
@@ -131,13 +124,6 @@
<x-reicon name="restart" class="size-3.5" x-show="!busy" />
<span x-text="busy ? 'Restarting…' : 'Restart'">Restart</span>
</x-slot:main>
@if ($isCacheDatabase)
<button type="button" class="listbox-option justify-start! gap-2.5!"
@click="open = false; document.getElementById('database-flush-trigger')?.click()" role="menuitem">
<x-reicon name="trash" class="size-3.5 text-error" />
Flush cache
</button>
@endif
<button type="button" class="listbox-option justify-start! gap-2.5!"
@click="open = false; document.getElementById('database-stop-trigger')?.click()" role="menuitem">
<x-reicon name="stop-circle" class="size-3.5 text-error" />
@@ -185,19 +171,6 @@
<button id="database-stop-trigger" type="button">Stop</button>
</x-slot:trigger>
</x-modal-confirmation>
@if ($isCacheDatabase)
<x-modal-confirmation title="Confirm Cache Flush?" buttonTitle="Flush cache" submitAction="flush"
:confirmWithText="true" :confirmationText="$database->name" :confirmWithPassword="false"
:actions="[
'Every key stored in this cache will be permanently erased.',
'This action cannot be undone and data cannot be recovered.',
'The database container keeps running; only its data is removed.',
]" step1ButtonText="Continue" step2ButtonText="Flush cache">
<x-slot:trigger>
<button id="database-flush-trigger" type="button">Flush cache</button>
</x-slot:trigger>
</x-modal-confirmation>
@endif
</div>
@endif
+35 -31
View File
@@ -1,7 +1,7 @@
<?php
use App\Actions\Database\FlushCacheDatabase;
use App\Livewire\Project\Database\Heading;
use App\Livewire\Project\Database\FlushCache;
use App\Models\AuditEvent;
use App\Models\Environment;
use App\Models\InstanceSettings;
@@ -49,12 +49,27 @@ function makeRedis(mixed $environment, mixed $destination): StandaloneRedis
]);
}
function makePostgres(mixed $environment, mixed $destination): StandalonePostgresql
{
return StandalonePostgresql::create([
'name' => 'app-postgres',
'image' => 'postgres:16',
'postgres_user' => 'coolify',
'postgres_password' => 'password',
'postgres_db' => 'coolify',
'status' => 'running:healthy',
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
}
test('flushes the cache of a running redis database and records an audit event', function () {
$redis = makeRedis($this->environment, $this->destination);
FlushCacheDatabase::mock()->shouldReceive('handle')->once();
Livewire::test(Heading::class, ['database' => $redis])
Livewire::test(FlushCache::class, ['database' => $redis])
->call('flush')
->assertDispatched('success');
@@ -65,21 +80,11 @@ test('flushes the cache of a running redis database and records an audit event',
});
test('refuses to flush a non-cache database and does not run the action', function () {
$postgres = StandalonePostgresql::create([
'name' => 'app-postgres',
'image' => 'postgres:16',
'postgres_user' => 'coolify',
'postgres_password' => 'password',
'postgres_db' => 'coolify',
'status' => 'running:healthy',
'environment_id' => $this->environment->id,
'destination_id' => $this->destination->id,
'destination_type' => $this->destination->getMorphClass(),
]);
$postgres = makePostgres($this->environment, $this->destination);
FlushCacheDatabase::mock()->shouldReceive('handle')->never();
Livewire::test(Heading::class, ['database' => $postgres])
Livewire::test(FlushCache::class, ['database' => $postgres])
->call('flush')
->assertDispatched('error');
@@ -96,31 +101,30 @@ test('denies flushing to a member without manage permission', function () {
FlushCacheDatabase::mock()->shouldReceive('handle')->never();
Livewire::test(Heading::class, ['database' => $redis])
Livewire::test(FlushCache::class, ['database' => $redis])
->call('flush')
->assertDispatched('error');
expect(AuditEvent::query()->where('event', 'ui.database.flushed')->exists())->toBeFalse();
});
test('only cache databases expose the flush cache action in the heading', function () {
test('renders the flush cache card for a cache database', function () {
$redis = makeRedis($this->environment, $this->destination);
Livewire::test(Heading::class, ['database' => $redis])
->assertSee('Flush cache');
Livewire::test(FlushCache::class, ['database' => $redis])
->assertSee('Flush cache')
->assertSee('FLUSHALL ASYNC');
});
$postgres = StandalonePostgresql::create([
'name' => 'app-postgres',
'image' => 'postgres:16',
'postgres_user' => 'coolify',
'postgres_password' => 'password',
'postgres_db' => 'coolify',
'status' => 'running:healthy',
'environment_id' => $this->environment->id,
'destination_id' => $this->destination->id,
'destination_type' => $this->destination->getMorphClass(),
]);
test('the danger page wires the flush cache section only for cache database types', function () {
$source = file_get_contents(resource_path('views/livewire/project/database/configuration.blade.php'));
Livewire::test(Heading::class, ['database' => $postgres])
->assertDontSee('Flush cache');
// The flush-cache component is rendered on the danger route, guarded to the three cache types.
expect($source)
->toContain('project.database.flush-cache')
->toMatch('/in_array\(\$database->type\(\), \[.*standalone-redis.*standalone-keydb.*standalone-dragonfly.*\]\)[\s\S]*project\.database\.flush-cache/');
// It must no longer live in the heading action menu.
$heading = file_get_contents(resource_path('views/livewire/project/database/heading.blade.php'));
expect($heading)->not->toContain('Flush cache');
});