mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
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:
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user