Merge branch 'next' into fix-ssh-keys

This commit is contained in:
Andras Bacsai
2024-09-19 11:45:12 +02:00
committed by GitHub
70 changed files with 1693 additions and 498 deletions

View File

@@ -3,50 +3,40 @@
namespace App\Actions\Application; namespace App\Actions\Application;
use App\Models\Application; use App\Models\Application;
use App\Actions\Server\CleanupDocker;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
class StopApplication class StopApplication
{ {
use AsAction; use AsAction;
public function handle(Application $application, bool $previewDeployments = false) public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true)
{ {
if ($application->destination->server->isSwarm()) { try {
instant_remote_process(["docker stack rm {$application->uuid}"], $application->destination->server); $server = $application->destination->server;
return;
}
$servers = collect([]);
$servers->push($application->destination->server);
$application->additional_servers->map(function ($server) use ($servers) {
$servers->push($server);
});
foreach ($servers as $server) {
if (!$server->isFunctional()) { if (!$server->isFunctional()) {
return 'Server is not functional'; return 'Server is not functional';
} }
if ($previewDeployments) { ray('Stopping application: ' . $application->name);
$containers = getCurrentApplicationContainerStatus($server, $application->id, includePullrequests: true);
} else { if ($server->isSwarm()) {
$containers = getCurrentApplicationContainerStatus($server, $application->id, 0); instant_remote_process(["docker stack rm {$application->uuid}"], $server);
} return;
if ($containers->count() > 0) {
foreach ($containers as $container) {
$containerName = data_get($container, 'Names');
if ($containerName) {
instant_remote_process(command: ["docker stop --time=30 $containerName"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm $containerName"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm -f {$containerName}"], server: $server, throwError: false);
}
}
} }
$containersToStop = $application->getContainersToStop($previewDeployments);
$application->stopContainers($containersToStop, $server);
if ($application->build_pack === 'dockercompose') { if ($application->build_pack === 'dockercompose') {
// remove network $application->delete_connected_networks($application->uuid);
$uuid = $application->uuid; }
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
instant_remote_process(["docker network rm {$uuid}"], $server, false); if ($dockerCleanup) {
} CleanupDocker::run($server, true);
}
} catch (\Exception $e) {
ray($e->getMessage());
return $e->getMessage();
} }
} }
} }

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Database; namespace App\Actions\Database;
use App\Actions\Server\CleanupDocker;
use App\Models\StandaloneClickhouse; use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly; use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb; use App\Models\StandaloneKeydb;
@@ -10,25 +11,65 @@ use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql; use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis; use App\Models\StandaloneRedis;
use Illuminate\Support\Facades\Process;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
class StopDatabase class StopDatabase
{ {
use AsAction; use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database) public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database, bool $isDeleteOperation = false, bool $dockerCleanup = true)
{ {
$server = $database->destination->server; $server = $database->destination->server;
if (! $server->isFunctional()) { if (! $server->isFunctional()) {
return 'Server is not functional'; return 'Server is not functional';
} }
instant_remote_process(command: ["docker stop --time=30 $database->uuid"], server: $server, throwError: false); $this->stopContainer($database, $database->uuid, 300);
instant_remote_process(command: ["docker rm $database->uuid"], server: $server, throwError: false); if (! $isDeleteOperation) {
instant_remote_process(command: ["docker rm -f $database->uuid"], server: $server, throwError: false); if ($dockerCleanup) {
CleanupDocker::run($server, true);
}
}
if ($database->is_public) { if ($database->is_public) {
StopDatabaseProxy::run($database); StopDatabaseProxy::run($database);
} }
return 'Database stopped successfully';
}
private function stopContainer($database, string $containerName, int $timeout = 300): void
{
$server = $database->destination->server;
$process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
$startTime = time();
while ($process->running()) {
if (time() - $startTime >= $timeout) {
$this->forceStopContainer($containerName, $server);
break;
}
usleep(100000);
}
$this->removeContainer($containerName, $server);
}
private function forceStopContainer(string $containerName, $server): void
{
instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
}
private function removeContainer(string $containerName, $server): void
{
instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
}
private function deleteConnectedNetworks($uuid, $server)
{
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
instant_remote_process(["docker network rm {$uuid}"], $server, false);
} }
} }

View File

@@ -3,17 +3,18 @@
namespace App\Actions\Service; namespace App\Actions\Service;
use App\Models\Service; use App\Models\Service;
use App\Actions\Server\CleanupDocker;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
class DeleteService class DeleteService
{ {
use AsAction; use AsAction;
public function handle(Service $service) public function handle(Service $service, bool $deleteConfigurations, bool $deleteVolumes, bool $dockerCleanup, bool $deleteConnectedNetworks)
{ {
try { try {
$server = data_get($service, 'server'); $server = data_get($service, 'server');
if ($server->isFunctional()) { if ($deleteVolumes && $server->isFunctional()) {
$storagesToDelete = collect([]); $storagesToDelete = collect([]);
$service->environment_variables()->delete(); $service->environment_variables()->delete();
@@ -33,13 +34,29 @@ class DeleteService
foreach ($storagesToDelete as $storage) { foreach ($storagesToDelete as $storage) {
$commands[] = "docker volume rm -f $storage->name"; $commands[] = "docker volume rm -f $storage->name";
} }
$commands[] = "docker rm -f $service->uuid";
instant_remote_process($commands, $server, false); // Execute volume deletion first, this must be done first otherwise volumes will not be deleted.
if (!empty($commands)) {
foreach ($commands as $command) {
$result = instant_remote_process([$command], $server, false);
if ($result !== 0) {
ray("Failed to execute: $command");
} }
}
}
}
if ($deleteConnectedNetworks) {
$service->delete_connected_networks($service->uuid);
}
instant_remote_process(["docker rm -f $service->uuid"], $server, throwError: false);
} catch (\Exception $e) { } catch (\Exception $e) {
throw new \Exception($e->getMessage()); throw new \Exception($e->getMessage());
} finally { } finally {
if ($deleteConfigurations) {
$service->delete_configurations();
}
foreach ($service->applications()->get() as $application) { foreach ($service->applications()->get() as $application) {
$application->forceDelete(); $application->forceDelete();
} }
@@ -50,6 +67,11 @@ class DeleteService
$task->delete(); $task->delete();
} }
$service->tags()->detach(); $service->tags()->detach();
$service->forceDelete();
if ($dockerCleanup) {
CleanupDocker::run($server, true);
}
} }
} }
} }

View File

@@ -3,46 +3,33 @@
namespace App\Actions\Service; namespace App\Actions\Service;
use App\Models\Service; use App\Models\Service;
use App\Actions\Server\CleanupDocker;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
class StopService class StopService
{ {
use AsAction; use AsAction;
public function handle(Service $service) public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true)
{ {
try { try {
$server = $service->destination->server; $server = $service->destination->server;
if (!$server->isFunctional()) { if (!$server->isFunctional()) {
return 'Server is not functional'; return 'Server is not functional';
} }
ray('Stopping service: '.$service->name);
$applications = $service->applications()->get();
foreach ($applications as $application) {
if ($applications->count() < 6) {
instant_remote_process(command: ["docker stop --time=10 {$application->name}-{$service->uuid}"], server: $server, throwError: false);
}
instant_remote_process(command: ["docker rm {$application->name}-{$service->uuid}"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm -f {$application->name}-{$service->uuid}"], server: $server, throwError: false);
$application->update(['status' => 'exited']);
}
$dbs = $service->databases()->get();
foreach ($dbs as $db) {
if ($dbs->count() < 6) {
instant_remote_process(command: ["docker stop --time=10 {$db->name}-{$service->uuid}"], server: $server, throwError: false); $containersToStop = $service->getContainersToStop();
$service->stopContainers($containersToStop, $server);
if (!$isDeleteOperation) {
$service->delete_connected_networks($service->uuid);
if ($dockerCleanup) {
CleanupDocker::run($server, true);
} }
instant_remote_process(command: ["docker rm {$db->name}-{$service->uuid}"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm -f {$db->name}-{$service->uuid}"], server: $server, throwError: false);
$db->update(['status' => 'exited']);
} }
instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy"], $service->server);
instant_remote_process(["docker network rm {$service->uuid}"], $service->server);
} catch (\Exception $e) { } catch (\Exception $e) {
ray($e->getMessage()); ray($e->getMessage());
return $e->getMessage(); return $e->getMessage();
} }
} }
} }

View File

@@ -29,6 +29,7 @@ use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Sleep; use Illuminate\Support\Sleep;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Support\Facades\Process;
use RuntimeException; use RuntimeException;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use Throwable; use Throwable;
@@ -2200,20 +2201,40 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->application_deployment_queue->addLogEntry('Building docker image completed.'); $this->application_deployment_queue->addLogEntry('Building docker image completed.');
} }
/** private function graceful_shutdown_container(string $containerName, int $timeout = 300)
* @param int $timeout in seconds
*/
private function graceful_shutdown_container(string $containerName, int $timeout = 30)
{ {
try { try {
$process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
$startTime = time();
while ($process->running()) {
if (time() - $startTime >= $timeout) {
$this->execute_remote_command( $this->execute_remote_command(
["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true], ["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
["docker rm $containerName", 'hidden' => true, 'ignore_errors' => true]
); );
} catch (\Exception $error) { break;
// report error if needed }
usleep(100000);
} }
$isRunning = $this->execute_remote_command(
["docker inspect -f '{{.State.Running}}' $containerName", 'hidden' => true, 'ignore_errors' => true]
) === 'true';
if ($isRunning) {
$this->execute_remote_command(
["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
);
}
} catch (\Exception $error) {
$this->application_deployment_queue->addLogEntry("Error stopping container $containerName: " . $error->getMessage(), 'stderr');
}
$this->remove_container($containerName);
}
private function remove_container(string $containerName)
{
$this->execute_remote_command( $this->execute_remote_command(
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true] ["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
); );

View File

@@ -4,6 +4,7 @@ namespace App\Jobs;
use App\Actions\Application\StopApplication; use App\Actions\Application\StopApplication;
use App\Actions\Database\StopDatabase; use App\Actions\Database\StopDatabase;
use App\Actions\Server\CleanupDocker;
use App\Actions\Service\DeleteService; use App\Actions\Service\DeleteService;
use App\Actions\Service\StopService; use App\Actions\Service\StopService;
use App\Models\Application; use App\Models\Application;
@@ -30,8 +31,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
public function __construct( public function __construct(
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource,
public bool $deleteConfigurations = false, public bool $deleteConfigurations,
public bool $deleteVolumes = false) {} public bool $deleteVolumes,
public bool $dockerCleanup,
public bool $deleteConnectedNetworks
) {}
public function handle() public function handle()
{ {
@@ -51,11 +55,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
case 'standalone-dragonfly': case 'standalone-dragonfly':
case 'standalone-clickhouse': case 'standalone-clickhouse':
$persistentStorages = $this->resource?->persistentStorages()?->get(); $persistentStorages = $this->resource?->persistentStorages()?->get();
StopDatabase::run($this->resource); StopDatabase::run($this->resource, true);
break; break;
case 'service': case 'service':
StopService::run($this->resource); StopService::run($this->resource, true);
DeleteService::run($this->resource); DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks);
break; break;
} }
@@ -65,12 +69,31 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
if ($this->deleteConfigurations) { if ($this->deleteConfigurations) {
$this->resource?->delete_configurations(); $this->resource?->delete_configurations();
} }
$isDatabase = $this->resource instanceof StandalonePostgresql
|| $this->resource instanceof StandaloneRedis
|| $this->resource instanceof StandaloneMongodb
|| $this->resource instanceof StandaloneMysql
|| $this->resource instanceof StandaloneMariadb
|| $this->resource instanceof StandaloneKeydb
|| $this->resource instanceof StandaloneDragonfly
|| $this->resource instanceof StandaloneClickhouse;
$server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server');
if (($this->dockerCleanup || $isDatabase) && $server) {
CleanupDocker::run($server, true);
}
if ($this->deleteConnectedNetworks && ! $isDatabase) {
$this->resource?->delete_connected_networks($this->resource->uuid);
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
ray($e->getMessage());
send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage()); send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
throw $e; throw $e;
} finally { } finally {
$this->resource->forceDelete(); $this->resource->forceDelete();
if ($this->dockerCleanup) {
CleanupDocker::run($server, true);
}
Artisan::queue('cleanup:stucked-resources'); Artisan::queue('cleanup:stucked-resources');
} }
} }

View File

@@ -30,7 +30,6 @@ class Dashboard extends Component
public function cleanup_queue() public function cleanup_queue()
{ {
$this->dispatch('success', 'Cleanup started.');
Artisan::queue('cleanup:application-deployment-queue', [ Artisan::queue('cleanup:application-deployment-queue', [
'--team-id' => currentTeam()->id, '--team-id' => currentTeam()->id,
]); ]);

View File

@@ -1,7 +1,6 @@
<?php <?php
namespace App\Livewire\Destination; namespace App\Livewire\Destination;
use Livewire\Component; use Livewire\Component;
class Form extends Component class Form extends Component
@@ -38,7 +37,7 @@ class Form extends Component
} }
$this->destination->delete(); $this->destination->delete();
return redirect()->route('dashboard'); return redirect()->route('destination.all');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -4,11 +4,25 @@ namespace App\Livewire;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Livewire\Component; use Livewire\Component;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class NavbarDeleteTeam extends Component class NavbarDeleteTeam extends Component
{ {
public function delete() public $team;
public function mount()
{ {
$this->team = currentTeam()->name;
}
public function delete($password)
{
if (!Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
$currentTeam = currentTeam(); $currentTeam = currentTeam();
$currentTeam->delete(); $currentTeam->delete();

View File

@@ -21,6 +21,8 @@ class Heading extends Component
protected string $deploymentUuid; protected string $deploymentUuid;
public bool $docker_cleanup = true;
public function getListeners() public function getListeners()
{ {
$teamId = auth()->user()->currentTeam()->id; $teamId = auth()->user()->currentTeam()->id;
@@ -102,7 +104,7 @@ class Heading extends Component
public function stop() public function stop()
{ {
StopApplication::run($this->application); StopApplication::run($this->application, false, $this->docker_cleanup);
$this->application->status = 'exited'; $this->application->status = 'exited';
$this->application->save(); $this->application->save();
if ($this->application->additional_servers->count() > 0) { if ($this->application->additional_servers->count() > 0) {
@@ -135,4 +137,13 @@ class Heading extends Component
'environment_name' => $this->parameters['environment_name'], 'environment_name' => $this->parameters['environment_name'],
]); ]);
} }
public function render()
{
return view('livewire.project.application.heading', [
'checkboxes' => [
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
],
]);
}
} }

View File

@@ -9,6 +9,8 @@ use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url; use Spatie\Url\Url;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Facades\Process;
class Previews extends Component class Previews extends Component
{ {
@@ -184,17 +186,20 @@ class Previews extends Component
public function stop(int $pull_request_id) public function stop(int $pull_request_id)
{ {
try { try {
$server = $this->application->destination->server;
$timeout = 300;
if ($this->application->destination->server->isSwarm()) { if ($this->application->destination->server->isSwarm()) {
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server); instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
} else { } else {
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id); $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
foreach ($containers as $container) { $this->stopContainers($containers, $server, $timeout);
$name = str_replace('/', '', $container['Names']);
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
} }
}
GetContainersStatus::dispatchSync($this->application->destination->server)->onQueue('high'); GetContainersStatus::run($server);
$this->dispatch('reloadWindow'); $this->application->refresh();
$this->dispatch('containerStatusUpdated');
$this->dispatch('success', 'Preview Deployment stopped.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
@@ -203,16 +208,21 @@ class Previews extends Component
public function delete(int $pull_request_id) public function delete(int $pull_request_id)
{ {
try { try {
$server = $this->application->destination->server;
$timeout = 300;
if ($this->application->destination->server->isSwarm()) { if ($this->application->destination->server->isSwarm()) {
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server); instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
} else { } else {
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id); $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
foreach ($containers as $container) { $this->stopContainers($containers, $server, $timeout);
$name = str_replace('/', '', $container['Names']);
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
} }
}
ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first()->delete(); ApplicationPreview::where('application_id', $this->application->id)
->where('pull_request_id', $pull_request_id)
->first()
->delete();
$this->application->refresh(); $this->application->refresh();
$this->dispatch('update_links'); $this->dispatch('update_links');
$this->dispatch('success', 'Preview deleted.'); $this->dispatch('success', 'Preview deleted.');
@@ -220,4 +230,49 @@ class Previews extends Component
return handleError($e, $this); return handleError($e, $this);
} }
} }
private function stopContainers(array $containers, $server, int $timeout)
{
$processes = [];
foreach ($containers as $container) {
$containerName = str_replace('/', '', $container['Names']);
$processes[$containerName] = $this->stopContainer($containerName, $timeout);
}
$startTime = time();
while (count($processes) > 0) {
$finishedProcesses = array_filter($processes, function ($process) {
return !$process->running();
});
foreach (array_keys($finishedProcesses) as $containerName) {
unset($processes[$containerName]);
$this->removeContainer($containerName, $server);
}
if (time() - $startTime >= $timeout) {
$this->forceStopRemainingContainers(array_keys($processes), $server);
break;
}
usleep(100000);
}
}
private function stopContainer(string $containerName, int $timeout): InvokedProcess
{
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
}
private function removeContainer(string $containerName, $server)
{
instant_remote_process(["docker rm -f $containerName"], $server, throwError: false);
}
private function forceStopRemainingContainers(array $containerNames, $server)
{
foreach ($containerNames as $containerName) {
instant_remote_process(["docker kill $containerName"], $server, throwError: false);
$this->removeContainer($containerName, $server);
}
}
} }

View File

@@ -5,6 +5,8 @@ namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url; use Spatie\Url\Url;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
class BackupEdit extends Component class BackupEdit extends Component
{ {
@@ -12,6 +14,10 @@ class BackupEdit extends Component
public $s3s; public $s3s;
public bool $delete_associated_backups_locally = false;
public bool $delete_associated_backups_s3 = false;
public bool $delete_associated_backups_sftp = false;
public ?string $status = null; public ?string $status = null;
public array $parameters; public array $parameters;
@@ -46,10 +52,23 @@ class BackupEdit extends Component
} }
} }
public function delete() public function delete($password)
{ {
if (!Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
try { try {
if ($this->delete_associated_backups_locally) {
$this->deleteAssociatedBackupsLocally();
}
if ($this->delete_associated_backups_s3) {
$this->deleteAssociatedBackupsS3();
}
$this->backup->delete(); $this->backup->delete();
if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') { if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
$previousUrl = url()->previous(); $previousUrl = url()->previous();
$url = Url::fromString($previousUrl); $url = Url::fromString($previousUrl);
@@ -104,4 +123,66 @@ class BackupEdit extends Component
$this->dispatch('error', $e->getMessage()); $this->dispatch('error', $e->getMessage());
} }
} }
public function deleteAssociatedBackupsLocally()
{
$executions = $this->backup->executions;
$backupFolder = null;
foreach ($executions as $execution) {
if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
$server = $this->backup->database->service->destination->server;
} else {
$server = $this->backup->database->destination->server;
}
if (!$backupFolder) {
$backupFolder = dirname($execution->filename);
}
delete_backup_locally($execution->filename, $server);
$execution->delete();
}
if ($backupFolder) {
$this->deleteEmptyBackupFolder($backupFolder, $server);
}
}
public function deleteAssociatedBackupsS3()
{
//Add function to delete backups from S3
}
public function deleteAssociatedBackupsSftp()
{
//Add function to delete backups from SFTP
}
private function deleteEmptyBackupFolder($folderPath, $server)
{
$checkEmpty = instant_remote_process(["[ -z \"$(ls -A '$folderPath')\" ] && echo 'empty' || echo 'not empty'"], $server);
if (trim($checkEmpty) === 'empty') {
instant_remote_process(["rmdir '$folderPath'"], $server);
$parentFolder = dirname($folderPath);
$checkParentEmpty = instant_remote_process(["[ -z \"$(ls -A '$parentFolder')\" ] && echo 'empty' || echo 'not empty'"], $server);
if (trim($checkParentEmpty) === 'empty') {
instant_remote_process(["rmdir '$parentFolder'"], $server);
}
}
}
public function render()
{
return view('livewire.project.database.backup-edit', [
'checkboxes' => [
['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from local storage.'],
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected S3 Storage.']
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected SFTP Storage.']
]
]);
}
} }

View File

@@ -4,6 +4,9 @@ namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use Livewire\Component; use Livewire\Component;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\On;
class BackupExecutions extends Component class BackupExecutions extends Component
{ {
@@ -12,9 +15,12 @@ class BackupExecutions extends Component
public $executions = []; public $executions = [];
public $setDeletableBackup; public $setDeletableBackup;
public $delete_backup_s3 = true;
public $delete_backup_sftp = true;
public function getListeners() public function getListeners()
{ {
$userId = auth()->user()->id; $userId = Auth::id();
return [ return [
"echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions', "echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions',
@@ -31,19 +37,34 @@ class BackupExecutions extends Component
} }
} }
public function deleteBackup($exeuctionId) #[On('deleteBackup')]
public function deleteBackup($executionId, $password)
{ {
$execution = $this->backup->executions()->where('id', $exeuctionId)->first(); if (!Hash::check($password, Auth::user()->password)) {
if (is_null($execution)) { $this->addError('password', 'The provided password is incorrect.');
$this->dispatch('error', 'Backup execution not found.');
return; return;
} }
$execution = $this->backup->executions()->where('id', $executionId)->first();
if (is_null($execution)) {
$this->dispatch('error', 'Backup execution not found.');
return;
}
if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') { if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server); delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server);
} else { } else {
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server); delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
} }
if ($this->delete_backup_s3) {
// Add logic to delete from S3
}
if ($this->delete_backup_sftp) {
// Add logic to delete from SFTP
}
$execution->delete(); $execution->delete();
$this->dispatch('success', 'Backup deleted.'); $this->dispatch('success', 'Backup deleted.');
$this->refreshBackupExecutions(); $this->refreshBackupExecutions();
@@ -106,4 +127,14 @@ class BackupExecutions extends Component
} }
return $dateObj->format('Y-m-d H:i:s T'); return $dateObj->format('Y-m-d H:i:s T');
} }
public function render()
{
return view('livewire.project.database.backup-executions', [
'checkboxes' => [
['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently form S3 Storage'],
['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'],
]
]);
}
} }

View File

@@ -14,6 +14,8 @@ class Heading extends Component
public array $parameters; public array $parameters;
public $docker_cleanup = true;
public function getListeners() public function getListeners()
{ {
$userId = auth()->user()->id; $userId = auth()->user()->id;
@@ -54,7 +56,7 @@ class Heading extends Component
public function stop() public function stop()
{ {
StopDatabase::run($this->database); StopDatabase::run($this->database, false, $this->docker_cleanup);
$this->database->status = 'exited'; $this->database->status = 'exited';
$this->database->save(); $this->database->save();
$this->check_status(); $this->check_status();
@@ -71,4 +73,13 @@ class Heading extends Component
$activity = StartDatabase::run($this->database); $activity = StartDatabase::run($this->database);
$this->dispatch('activityMonitor', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} }
public function render()
{
return view('livewire.project.database.heading', [
'checkboxes' => [
['id' => 'docker_cleanup', 'label' => 'Cleanup docker build cache and unused images (next deployment could take longer).'],
],
]);
}
} }

View File

@@ -13,9 +13,12 @@ class DeleteEnvironment extends Component
public bool $disabled = false; public bool $disabled = false;
public string $environmentName = '';
public function mount() public function mount()
{ {
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->environmentName = Environment::findOrFail($this->environment_id)->name;
} }
public function delete() public function delete()

View File

@@ -13,9 +13,12 @@ class DeleteProject extends Component
public bool $disabled = false; public bool $disabled = false;
public string $projectName = '';
public function mount() public function mount()
{ {
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->projectName = Project::findOrFail($this->project_id)->name;
} }
public function delete() public function delete()

View File

@@ -52,7 +52,7 @@ class Configuration extends Component
$application = $this->service->applications->find($id); $application = $this->service->applications->find($id);
if ($application) { if ($application) {
$application->restart(); $application->restart();
$this->dispatch('success', 'Application restarted successfully.'); $this->dispatch('success', 'Service application restarted successfully.');
} }
} catch (\Exception $e) { } catch (\Exception $e) {
return handleError($e, $this); return handleError($e, $this);
@@ -65,7 +65,7 @@ class Configuration extends Component
$database = $this->service->databases->find($id); $database = $this->service->databases->find($id);
if ($database) { if ($database) {
$database->restart(); $database->restart();
$this->dispatch('success', 'Database restarted successfully.'); $this->dispatch('success', 'Service database restarted successfully.');
} }
} catch (\Exception $e) { } catch (\Exception $e) {
return handleError($e, $this); return handleError($e, $this);

View File

@@ -15,6 +15,8 @@ use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis; use App\Models\StandaloneRedis;
use Livewire\Component; use Livewire\Component;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
class FileStorage extends Component class FileStorage extends Component
{ {
@@ -83,8 +85,13 @@ class FileStorage extends Component
} }
} }
public function delete() public function delete($password)
{ {
if (!Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
try { try {
$message = 'File deleted.'; $message = 'File deleted.';
if ($this->fileStorage->is_directory) { if ($this->fileStorage->is_directory) {
@@ -129,6 +136,13 @@ class FileStorage extends Component
public function render() public function render()
{ {
return view('livewire.project.service.file-storage'); return view('livewire.project.service.file-storage', [
'directoryDeletionCheckboxes' => [
['id' => 'permanently_delete', 'label' => 'The selected directory and all its contents will be permantely deleted form the server.'],
],
'fileDeletionCheckboxes' => [
['id' => 'permanently_delete', 'label' => 'The selected file will be permanently deleted form the server.'],
]
]);
} }
} }

View File

@@ -20,6 +20,8 @@ class Navbar extends Component
public $isDeploymentProgress = false; public $isDeploymentProgress = false;
public $docker_cleanup = true;
public $title = 'Configuration'; public $title = 'Configuration';
public function mount() public function mount()
@@ -42,7 +44,7 @@ class Navbar extends Component
public function serviceStarted() public function serviceStarted()
{ {
$this->dispatch('success', 'Service status changed.'); // $this->dispatch('success', 'Service status changed.');
if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) { if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) {
$this->service->isConfigurationChanged(true); $this->service->isConfigurationChanged(true);
$this->dispatch('configurationChanged'); $this->dispatch('configurationChanged');
@@ -62,11 +64,6 @@ class Navbar extends Component
$this->dispatch('success', 'Service status updated.'); $this->dispatch('success', 'Service status updated.');
} }
public function render()
{
return view('livewire.project.service.navbar');
}
public function checkDeployments() public function checkDeployments()
{ {
try { try {
@@ -97,14 +94,9 @@ class Navbar extends Component
$this->dispatch('activityMonitor', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} }
public function stop(bool $forceCleanup = false) public function stop()
{ {
StopService::run($this->service); StopService::run($this->service, false, $this->docker_cleanup);
if ($forceCleanup) {
$this->dispatch('success', 'Containers cleaned up.');
} else {
$this->dispatch('success', 'Service stopped.');
}
ServiceStatusChanged::dispatch(); ServiceStatusChanged::dispatch();
} }
@@ -123,4 +115,13 @@ class Navbar extends Component
$activity = StartService::run($this->service); $activity = StartService::run($this->service);
$this->dispatch('activityMonitor', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} }
public function render()
{
return view('livewire.project.service.navbar', [
'checkboxes' => [
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
],
]);
}
} }

View File

@@ -3,6 +3,8 @@
namespace App\Livewire\Project\Service; namespace App\Livewire\Project\Service;
use App\Models\ServiceApplication; use App\Models\ServiceApplication;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
class ServiceApplicationView extends Component class ServiceApplicationView extends Component
@@ -11,6 +13,10 @@ class ServiceApplicationView extends Component
public $parameters; public $parameters;
public $docker_cleanup = true;
public $delete_volumes = true;
protected $rules = [ protected $rules = [
'application.human_name' => 'nullable', 'application.human_name' => 'nullable',
'application.description' => 'nullable', 'application.description' => 'nullable',
@@ -23,11 +29,6 @@ class ServiceApplicationView extends Component
'application.is_stripprefix_enabled' => 'nullable|boolean', 'application.is_stripprefix_enabled' => 'nullable|boolean',
]; ];
public function render()
{
return view('livewire.project.service.service-application-view');
}
public function updatedApplicationFqdn() public function updatedApplicationFqdn()
{ {
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
@@ -56,8 +57,14 @@ class ServiceApplicationView extends Component
$this->dispatch('success', 'You need to restart the service for the changes to take effect.'); $this->dispatch('success', 'You need to restart the service for the changes to take effect.');
} }
public function delete() public function delete($password)
{ {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
try { try {
$this->application->delete(); $this->application->delete();
$this->dispatch('success', 'Application deleted.'); $this->dispatch('success', 'Application deleted.');
@@ -91,4 +98,17 @@ class ServiceApplicationView extends Component
$this->dispatch('generateDockerCompose'); $this->dispatch('generateDockerCompose');
} }
} }
public function render()
{
return view('livewire.project.service.service-application-view', [
'checkboxes' => [
['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
// ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
],
]);
}
} }

View File

@@ -3,6 +3,11 @@
namespace App\Livewire\Project\Shared; namespace App\Livewire\Project\Shared;
use App\Jobs\DeleteResourceJob; use App\Jobs\DeleteResourceJob;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@@ -10,6 +15,8 @@ class Danger extends Component
{ {
public $resource; public $resource;
public $resourceName;
public $projectUuid; public $projectUuid;
public $environmentName; public $environmentName;
@@ -18,22 +25,93 @@ class Danger extends Component
public bool $delete_volumes = true; public bool $delete_volumes = true;
public bool $docker_cleanup = true;
public bool $delete_connected_networks = true;
public ?string $modalId = null; public ?string $modalId = null;
public string $resourceDomain = '';
public function mount() public function mount()
{ {
$this->modalId = new Cuid2;
$parameters = get_route_parameters(); $parameters = get_route_parameters();
$this->modalId = new Cuid2;
$this->projectUuid = data_get($parameters, 'project_uuid'); $this->projectUuid = data_get($parameters, 'project_uuid');
$this->environmentName = data_get($parameters, 'environment_name'); $this->environmentName = data_get($parameters, 'environment_name');
if ($this->resource === null) {
if (isset($parameters['service_uuid'])) {
$this->resource = Service::where('uuid', $parameters['service_uuid'])->first();
} elseif (isset($parameters['stack_service_uuid'])) {
$this->resource = ServiceApplication::where('uuid', $parameters['stack_service_uuid'])->first()
?? ServiceDatabase::where('uuid', $parameters['stack_service_uuid'])->first();
}
} }
public function delete() if ($this->resource === null) {
$this->resourceName = 'Unknown Resource';
return;
}
if (! method_exists($this->resource, 'type')) {
$this->resourceName = 'Unknown Resource';
return;
}
switch ($this->resource->type()) {
case 'application':
$this->resourceName = $this->resource->name ?? 'Application';
break;
case 'standalone-postgresql':
case 'standalone-redis':
case 'standalone-mongodb':
case 'standalone-mysql':
case 'standalone-mariadb':
case 'standalone-keydb':
case 'standalone-dragonfly':
case 'standalone-clickhouse':
$this->resourceName = $this->resource->name ?? 'Database';
break;
case 'service':
$this->resourceName = $this->resource->name ?? 'Service';
break;
case 'service-application':
$this->resourceName = $this->resource->name ?? 'Service Application';
break;
case 'service-database':
$this->resourceName = $this->resource->name ?? 'Service Database';
break;
default:
$this->resourceName = 'Unknown Resource';
}
}
public function delete($password)
{ {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
if (! $this->resource) {
$this->addError('resource', 'Resource not found.');
return;
}
try { try {
// $this->authorize('delete', $this->resource);
$this->resource->delete(); $this->resource->delete();
DeleteResourceJob::dispatch($this->resource, $this->delete_configurations, $this->delete_volumes); DeleteResourceJob::dispatch(
$this->resource,
$this->delete_configurations,
$this->delete_volumes,
$this->docker_cleanup,
$this->delete_connected_networks
);
return redirect()->route('project.resource.index', [ return redirect()->route('project.resource.index', [
'project_uuid' => $this->projectUuid, 'project_uuid' => $this->projectUuid,
@@ -43,4 +121,19 @@ class Danger extends Component
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function render()
{
return view('livewire.project.shared.danger', [
'checkboxes' => [
['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
['id' => 'delete_connected_networks', 'label' => __('resource.delete_connected_networks')],
['id' => 'delete_configurations', 'label' => __('resource.delete_configurations')],
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
// ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
],
]);
}
} }

View File

@@ -10,6 +10,8 @@ use App\Models\Server;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
class Destination extends Component class Destination extends Component
{ {
@@ -115,8 +117,13 @@ class Destination extends Component
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id')); ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
} }
public function removeServer(int $network_id, int $server_id) public function removeServer(int $network_id, int $server_id, $password)
{ {
if (!Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) { if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
$this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.'); $this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.');

View File

@@ -20,6 +20,8 @@ class Show extends Component
public string $type; public string $type;
public string $scheduledTaskName;
protected $rules = [ protected $rules = [
'task.enabled' => 'required|boolean', 'task.enabled' => 'required|boolean',
'task.name' => 'required|string', 'task.name' => 'required|string',
@@ -49,6 +51,7 @@ class Show extends Component
$this->modalId = new Cuid2; $this->modalId = new Cuid2;
$this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first(); $this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first();
$this->scheduledTaskName = $this->task->name;
} }
public function instantSave() public function instantSave()
@@ -75,9 +78,9 @@ class Show extends Component
$this->task->delete(); $this->task->delete();
if ($this->type == 'application') { if ($this->type == 'application') {
return redirect()->route('project.application.configuration', $this->parameters); return redirect()->route('project.application.configuration', $this->parameters, $this->scheduledTaskName);
} else { } else {
return redirect()->route('project.service.configuration', $this->parameters); return redirect()->route('project.service.configuration', $this->parameters, $this->scheduledTaskName);
} }
} catch (\Exception $e) { } catch (\Exception $e) {
return handleError($e); return handleError($e);

View File

@@ -3,6 +3,8 @@
namespace App\Livewire\Project\Shared\Storages; namespace App\Livewire\Project\Shared\Storages;
use App\Models\LocalPersistentVolume; use App\Models\LocalPersistentVolume;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
use Livewire\Component; use Livewire\Component;
class Show extends Component class Show extends Component
@@ -36,8 +38,13 @@ class Show extends Component
$this->dispatch('success', 'Storage updated successfully'); $this->dispatch('success', 'Storage updated successfully');
} }
public function delete() public function delete($password)
{ {
if (!Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
$this->storage->delete(); $this->storage->delete();
$this->dispatch('refreshStorages'); $this->dispatch('refreshStorages');
} }

View File

@@ -4,6 +4,8 @@ namespace App\Livewire\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
class Delete extends Component class Delete extends Component
{ {
@@ -11,8 +13,12 @@ class Delete extends Component
public $server; public $server;
public function delete() public function delete($password)
{ {
if (!Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
try { try {
$this->authorize('delete', $this->server); $this->authorize('delete', $this->server);
if ($this->server->hasDefinedResources()) { if ($this->server->hasDefinedResources()) {

View File

@@ -7,6 +7,8 @@ use App\Actions\Proxy\StartProxy;
use App\Events\ProxyStatusChanged; use App\Events\ProxyStatusChanged;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
use Illuminate\Support\Facades\Process;
use Illuminate\Process\InvokedProcess;
class Deploy extends Component class Deploy extends Component
{ {
@@ -94,21 +96,43 @@ class Deploy extends Component
public function stop(bool $forceStop = true) public function stop(bool $forceStop = true)
{ {
try { try {
if ($this->server->isSwarm()) { $containerName = $this->server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
instant_remote_process([ $timeout = 30;
'docker service rm coolify-proxy_traefik',
], $this->server); $process = $this->stopContainer($containerName, $timeout);
} else {
instant_remote_process([ $startTime = time();
'docker rm -f coolify-proxy', while ($process->running()) {
], $this->server); if (time() - $startTime >= $timeout) {
$this->forceStopContainer($containerName);
break;
} }
$this->server->proxy->status = 'exited'; usleep(100000);
$this->server->proxy->force_stop = $forceStop; }
$this->server->save();
$this->dispatch('proxyStatusUpdated'); $this->removeContainer($containerName);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} finally {
$this->server->proxy->force_stop = $forceStop;
$this->server->proxy->status = 'exited';
$this->server->save();
$this->dispatch('proxyStatusUpdated');
} }
} }
private function stopContainer(string $containerName, int $timeout): InvokedProcess
{
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
}
private function forceStopContainer(string $containerName)
{
instant_remote_process(["docker kill $containerName"], $this->server, throwError: false);
}
private function removeContainer(string $containerName)
{
instant_remote_process(["docker rm -f $containerName"], $this->server, throwError: false);
}
} }

View File

@@ -5,6 +5,8 @@ namespace App\Livewire\Team;
use App\Models\Team; use App\Models\Team;
use App\Models\User; use App\Models\User;
use Livewire\Component; use Livewire\Component;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class AdminView extends Component class AdminView extends Component
{ {
@@ -73,8 +75,12 @@ class AdminView extends Component
$team->delete(); $team->delete();
} }
public function delete($id) public function delete($id, $password)
{ {
if (!Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
if (! auth()->user()->isInstanceAdmin()) { if (! auth()->user()->isInstanceAdmin()) {
return $this->dispatch('error', 'You are not authorized to delete users'); return $this->dispatch('error', 'You are not authorized to delete users');
} }

View File

@@ -8,6 +8,8 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Support\Facades\Process;
use Illuminate\Process\InvokedProcess;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use RuntimeException; use RuntimeException;
use Spatie\Activitylog\Models\Activity; use Spatie\Activitylog\Models\Activity;
@@ -149,12 +151,64 @@ class Application extends BaseModel
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name'); return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
} }
public function getContainersToStop(bool $previewDeployments = false): array
{
$containers = $previewDeployments
? getCurrentApplicationContainerStatus($this->destination->server, $this->id, includePullrequests: true)
: getCurrentApplicationContainerStatus($this->destination->server, $this->id, 0);
return $containers->pluck('Names')->toArray();
}
public function stopContainers(array $containerNames, $server, int $timeout = 600)
{
$processes = [];
foreach ($containerNames as $containerName) {
$processes[$containerName] = $this->stopContainer($containerName, $server, $timeout);
}
$startTime = time();
while (count($processes) > 0) {
$finishedProcesses = array_filter($processes, function ($process) {
return !$process->running();
});
foreach ($finishedProcesses as $containerName => $process) {
unset($processes[$containerName]);
$this->removeContainer($containerName, $server);
}
if (time() - $startTime >= $timeout) {
$this->forceStopRemainingContainers(array_keys($processes), $server);
break;
}
usleep(100000);
}
}
public function stopContainer(string $containerName, $server, int $timeout): InvokedProcess
{
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
}
public function removeContainer(string $containerName, $server)
{
instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
}
public function forceStopRemainingContainers(array $containerNames, $server)
{
foreach ($containerNames as $containerName) {
instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
$this->removeContainer($containerName, $server);
}
}
public function delete_configurations() public function delete_configurations()
{ {
$server = data_get($this, 'destination.server'); $server = data_get($this, 'destination.server');
$workdir = $this->workdir(); $workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) { if (str($workdir)->endsWith($this->uuid)) {
ray('Deleting workdir');
instant_remote_process(['rm -rf ' . $this->workdir()], $server, false); instant_remote_process(['rm -rf ' . $this->workdir()], $server, false);
} }
} }
@@ -176,6 +230,14 @@ class Application extends BaseModel
} }
} }
public function delete_connected_networks($uuid)
{
$server = data_get($this, 'destination.server');
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
instant_remote_process(["docker network rm {$uuid}"], $server, false);
}
public function additional_servers() public function additional_servers()
{ {
return $this->belongsToMany(Server::class, 'additional_destinations') return $this->belongsToMany(Server::class, 'additional_destinations')
@@ -1166,7 +1228,6 @@ class Application extends BaseModel
} else { } else {
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name."); throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
} }
} }
public function parseContainerLabels(?ApplicationPreview $preview = null) public function parseContainerLabels(?ApplicationPreview $preview = null)

View File

@@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Spatie\Url\Url; use Spatie\Url\Url;
@@ -131,15 +133,80 @@ class Service extends BaseModel
return $this->morphToMany(Tag::class, 'taggable'); return $this->morphToMany(Tag::class, 'taggable');
} }
public function getContainersToStop(): array
{
$containersToStop = [];
$applications = $this->applications()->get();
foreach ($applications as $application) {
$containersToStop[] = "{$application->name}-{$this->uuid}";
}
$dbs = $this->databases()->get();
foreach ($dbs as $db) {
$containersToStop[] = "{$db->name}-{$this->uuid}";
}
return $containersToStop;
}
public function stopContainers(array $containerNames, $server, int $timeout = 300)
{
$processes = [];
foreach ($containerNames as $containerName) {
$processes[$containerName] = $this->stopContainer($containerName, $timeout);
}
$startTime = time();
while (count($processes) > 0) {
$finishedProcesses = array_filter($processes, function ($process) {
return !$process->running();
});
foreach (array_keys($finishedProcesses) as $containerName) {
unset($processes[$containerName]);
$this->removeContainer($containerName, $server);
}
if (time() - $startTime >= $timeout) {
$this->forceStopRemainingContainers(array_keys($processes), $server);
break;
}
usleep(100000);
}
}
public function stopContainer(string $containerName, int $timeout): InvokedProcess
{
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
}
public function removeContainer(string $containerName, $server)
{
instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
}
public function forceStopRemainingContainers(array $containerNames, $server)
{
foreach ($containerNames as $containerName) {
instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
$this->removeContainer($containerName, $server);
}
}
public function delete_configurations() public function delete_configurations()
{ {
$server = data_get($this, 'server'); $server = data_get($this, 'destination.server');
$workdir = $this->workdir(); $workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) { if (str($workdir)->endsWith($this->uuid)) {
instant_remote_process(['rm -rf ' . $this->workdir()], $server, false); instant_remote_process(['rm -rf ' . $this->workdir()], $server, false);
} }
} }
public function delete_connected_networks($uuid)
{
$server = data_get($this, 'destination.server');
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
instant_remote_process(["docker network rm {$uuid}"], $server, false);
}
public function status() public function status()
{ {
$applications = $this->applications; $applications = $this->applications;

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.341', 'release' => '4.0.0-beta.342',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.341'; return '4.0.0-beta.342';

View File

@@ -26,5 +26,11 @@
"input.code": "One-time code", "input.code": "One-time code",
"input.recovery_code": "Recovery code", "input.recovery_code": "Recovery code",
"button.save": "Save", "button.save": "Save",
"repository.url": "<span class='text-helper'>Examples</span><br>For Public repositories, use <span class='text-helper'>https://...</span>.<br>For Private repositories, use <span class='text-helper'>git@...</span>.<br><br>https://github.com/coollabsio/coolify-examples <span class='text-helper'>main</span> branch will be selected<br>https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify <span class='text-helper'>nodejs-fastify</span> branch will be selected.<br>https://gitea.com/sedlav/expressjs.git <span class='text-helper'>main</span> branch will be selected.<br>https://gitlab.com/andrasbacsai/nodejs-example.git <span class='text-helper'>main</span> branch will be selected." "repository.url": "<span class='text-helper'>Examples</span><br>For Public repositories, use <span class='text-helper'>https://...</span>.<br>For Private repositories, use <span class='text-helper'>git@...</span>.<br><br>https://github.com/coollabsio/coolify-examples <span class='text-helper'>main</span> branch will be selected<br>https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify <span class='text-helper'>nodejs-fastify</span> branch will be selected.<br>https://gitea.com/sedlav/expressjs.git <span class='text-helper'>main</span> branch will be selected.<br>https://gitlab.com/andrasbacsai/nodejs-example.git <span class='text-helper'>main</span> branch will be selected.",
"service.stop": "This service will be stopped.",
"resource.docker_cleanup": "Run Docker Cleanup (remove unused images and builder cache).",
"resource.non_persistent": "All non-persistent data will be deleted.",
"resource.delete_volumes": "All volumes associated with this resource will be permanently deleted.",
"resource.delete_connected_networks": "All non-predefined networks associated with this resource will be permanently deleted.",
"resource.delete_configurations": "All configuration files will be permanently deleted from the server."
} }

View File

@@ -1,4 +1,15 @@
@props([
'id',
'label' => null,
'helper' => null,
'disabled' => false,
'instantSave' => false,
'value' => null,
'hideLabel' => false,
])
<div class="flex flex-row items-center gap-4 px-2 py-1 form-control min-w-fit dark:hover:bg-coolgray-100"> <div class="flex flex-row items-center gap-4 px-2 py-1 form-control min-w-fit dark:hover:bg-coolgray-100">
@if(!$hideLabel)
<label class="flex gap-4 px-0 min-w-fit label"> <label class="flex gap-4 px-0 min-w-fit label">
<span class="flex gap-2"> <span class="flex gap-2">
@if ($label) @if ($label)
@@ -11,6 +22,7 @@
@endif @endif
</span> </span>
</label> </label>
@endif
<span class="flex-grow"></span> <span class="flex-grow"></span>
<input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }} <input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }}
@if ($instantSave) wire:loading.attr="disabled" wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}' @if ($instantSave) wire:loading.attr="disabled" wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'

View File

@@ -1,14 +1,105 @@
@props([ @props([
'title' => 'Are you sure?', 'title' => 'Are you sure?',
'isErrorButton' => false, 'isErrorButton' => false,
'buttonTitle' => 'REWRITE THIS BUTTON TITLE PLEASSSSEEEE', 'buttonTitle' => 'Confirm Action',
'buttonFullWidth' => false, 'buttonFullWidth' => false,
'customButton' => null, 'customButton' => null,
'disabled' => false, 'disabled' => false,
'action' => 'delete', 'submitAction' => 'delete',
'content' => null, 'content' => null,
'checkboxes' => [],
'actions' => [],
'confirmWithText' => true,
'confirmationText' => 'Confirm Deletion',
'confirmationLabel' => 'Please confirm the execution of the actions by entering the Name below',
'shortConfirmationLabel' => 'Name',
'confirmWithPassword' => true,
'step1ButtonText' => 'Continue',
'step2ButtonText' => 'Continue',
'step3ButtonText' => 'Confirm',
'dispatchEvent' => false,
'dispatchEventType' => 'success',
'dispatchEventMessage' => '',
]) ])
<div x-data="{ modalOpen: false }" @keydown.escape.window="modalOpen = false" :class="{ 'z-40': modalOpen }"
<div x-data="{
modalOpen: false,
step: {{ empty($checkboxes) ? 2 : 1 }},
initialStep: {{ empty($checkboxes) ? 2 : 1 }},
finalStep: {{ $confirmWithPassword ? 3 : 2 }},
deleteText: '',
password: '',
actions: @js($actions),
confirmationText: @js($confirmationText),
userConfirmationText: '',
confirmWithText: @js($confirmWithText),
confirmWithPassword: @js($confirmWithPassword),
copied: false,
submitAction: @js($submitAction),
passwordError: '',
selectedActions: @js(collect($checkboxes)->pluck('id')->filter(fn($id) => $this->$id)->values()->all()),
dispatchEvent: @js($dispatchEvent),
dispatchEventType: @js($dispatchEventType),
dispatchEventMessage: @js($dispatchEventMessage),
resetModal() {
this.step = this.initialStep;
this.deleteText = '';
this.password = '';
this.userConfirmationText = '';
this.selectedActions = @js(collect($checkboxes)->pluck('id')->filter(fn($id) => $this->$id)->values()->all());
$wire.$refresh();
},
step1ButtonText: @js($step1ButtonText),
step2ButtonText: @js($step2ButtonText),
step3ButtonText: @js($step3ButtonText),
validatePassword() {
if (this.confirmWithPassword && !this.password) {
return 'Password is required.';
}
return '';
},
submitForm() {
if (this.confirmWithPassword) {
this.passwordError = this.validatePassword();
if (this.passwordError) {
return Promise.resolve(this.passwordError);
}
}
const methodName = this.submitAction.split('(')[0];
const paramsMatch = this.submitAction.match(/\((.*?)\)/);
const params = paramsMatch ? paramsMatch[1].split(',').map(param => param.trim()) : [];
if (this.confirmWithPassword) {
params.push(this.password);
}
params.push(this.selectedActions);
return $wire[methodName](...params)
.then(result => {
if (result === true) {
return true;
} else if (typeof result === 'string') {
return result;
}
});
},
copyConfirmationText() {
navigator.clipboard.writeText(this.confirmationText);
this.copied = true;
setTimeout(() => {
this.copied = false;
}, 2000);
},
toggleAction(id) {
const index = this.selectedActions.indexOf(id);
if (index > -1) {
this.selectedActions.splice(index, 1);
} else {
this.selectedActions.push(id);
}
}
}" @keydown.escape.window="modalOpen = false; resetModal()" :class="{ 'z-40': modalOpen }"
class="relative w-auto h-auto"> class="relative w-auto h-auto">
@if ($customButton) @if ($customButton)
@if ($buttonFullWidth) @if ($buttonFullWidth)
@@ -60,11 +151,9 @@
@endif @endif
@endif @endif
<template x-teleport="body"> <template x-teleport="body">
<div x-show="modalOpen" <div x-show="modalOpen" @click.away="modalOpen = false; resetModal()"
class="fixed top-0 lg:pt-10 left-0 z-[99] flex items-start justify-center w-screen h-screen" x-cloak> class="fixed top-0 lg:pt-10 left-0 z-[99] flex items-start justify-center w-screen h-screen" x-cloak>
<div x-show="modalOpen" x-transition:enter="ease-out duration-100" x-transition:enter-start="opacity-0" <div x-show="modalOpen" @click="modalOpen = false; resetModal()"
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-100"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" @click="modalOpen=false"
class="absolute inset-0 w-full h-full bg-black bg-opacity-20 backdrop-blur-sm"></div> class="absolute inset-0 w-full h-full bg-black bg-opacity-20 backdrop-blur-sm"></div>
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100" <div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95" x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
@@ -72,56 +161,186 @@
x-transition:leave="ease-in duration-100" x-transition:leave="ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100" x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95" x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
class="relative w-full py-6 border rounded min-w-full lg:min-w-[36rem] max-w-fit bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300"> class="relative w-full py-6 border rounded min-w-full lg:min-w-[36rem] max-w-[48rem] bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300">
<div class="flex items-center justify-between pb-3"> <div class="flex items-center justify-between pb-3">
<h3 class="text-2xl font-bold">{{ $title }}</h3> <h3 class="text-2xl font-bold pr-8">{{ $title }}</h3>
{{-- <button @click="modalOpen=false" <button @click="modalOpen = false; resetModal()"
class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 rounded-full dark:text-white hover:bg-coolgray-300"> class="absolute top-2 right-2 flex items-center justify-center w-8 h-8 rounded-full dark:text-white hover:bg-coolgray-300">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" <svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor"> stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> --}} </button>
</div> </div>
<div class="relative w-auto pb-8"> <div class="relative w-auto pb-8">
{{ $slot }} @if (!empty($checkboxes))
<!-- Step 1: Select actions -->
<div x-show="step === 1">
<div class="flex justify-between items-center">
<h4>Actions</h4>
</div>
@foreach ($checkboxes as $index => $checkbox)
<div class="flex items-center justify-between mb-2">
<label for="{{ $checkbox['id'] }}"
class="text-sm leading-5 text-gray-700 dark:text-gray-300 flex-grow pr-4">
{{ $checkbox['label'] }}
</label>
<x-forms.checkbox :id="$checkbox['id']" :wire:model="$checkbox['id']"
x-on:change="toggleAction('{{ $checkbox['id'] }}')" :checked="$this->{$checkbox['id']}"
x-bind:checked="selectedActions.includes('{{ $checkbox['id'] }}')"
class="flex-shrink-0" :hideLabel="true" />
</div>
@endforeach
</div> </div>
<div class="flex flex-row justify-end space-x-2">
<x-forms.button @click="modalOpen=false"
class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">Cancel
</x-forms.button>
<div class="flex-1"></div>
@if ($attributes->whereStartsWith('wire:click')->first())
@if ($isErrorButton)
<x-forms.button @click="modalOpen=false" class="w-24" isError type="button"
wire:click.prevent="{{ $attributes->get('wire:click') }}">Continue
</x-forms.button>
@else
<x-forms.button @click="modalOpen=false" class="w-24" isHighlighted type="button"
wire:click.prevent="{{ $attributes->get('wire:click') }}">Continue
</x-forms.button>
@endif @endif
@elseif ($attributes->whereStartsWith('@click')->first())
@if ($isErrorButton) <!-- Step 2: Confirm deletion -->
<x-forms.button class="w-24" isError type="button" <div x-show="step === 2">
@click="modalOpen=false;{{ $attributes->get('@click') }}">Continue <div class="bg-error border-l-4 border-red-500 text-white p-4 mb-4" role="alert">
</x-forms.button> <p class="font-bold">Warning</p>
@else <p>This operation is permanent and cannot be undone. Please think again before proceeding!
<x-forms.button class="w-24" isHighlighted type="button" </p>
@click="modalOpen=false;{{ $attributes->get('@click') }}">Continue </div>
</x-forms.button> <div class="mb-4">The following actions will be performed:</div>
<ul class="mb-4 space-y-2">
@foreach ($actions as $action)
<li class="flex items-center text-red-500">
<svg class="w-5 h-5 mr-2 flex-shrink-0" fill="none" stroke="currentColor"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
</svg>
<span>{{ $action }}</span>
</li>
@endforeach
@foreach ($checkboxes as $checkbox)
<template x-if="selectedActions.includes('{{ $checkbox['id'] }}')">
<li class="flex items-center text-red-500">
<svg class="w-5 h-5 mr-2 flex-shrink-0" fill="none" stroke="currentColor"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
</svg>
<span>{{ $checkbox['label'] }}</span>
</li>
</template>
@endforeach
</ul>
@if ($confirmWithText)
<div class="mb-4">
<h4 class="text-lg font-semibold mb-2">Confirm Actions</h4>
<p class="text-sm mb-2">{{ $confirmationLabel }}</p>
<div class="relative mb-2">
<input type="text" x-model="confirmationText"
class="w-full p-2 pr-10 rounded text-black input cursor-text" readonly>
<button @click="copyConfirmationText()"
class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700"
title="Copy confirmation text" x-ref="copyButton">
<template x-if="!copied">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20"
fill="currentColor">
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
<path
d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
</svg>
</template>
<template x-if="copied">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-500"
viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</template>
</button>
</div>
<label for="userConfirmationText"
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mt-4">
{{ $shortConfirmationLabel }}
</label>
<input type="text" x-model="userConfirmationText"
class="w-full p-2 rounded text-black input mt-1">
</div>
@endif @endif
@elseif ($action) </div>
@if ($isErrorButton)
<x-forms.button @click="modalOpen=false" class="w-24" isError type="button" <!-- Step 3: Password confirmation -->
wire:click.prevent="{{ $action }}">Continue <div x-show="step === 3 && confirmWithPassword">
<div class="bg-error border-l-4 border-red-500 text-white p-4 mb-4" role="alert">
<p class="font-bold">Final Confirmation</p>
<p>Please enter your password to confirm this destructive action.</p>
</div>
<div class="flex flex-col gap-2 mb-4">
<label for="password-confirm"
class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Your Password
</label>
<input type="password" id="password-confirm" x-model="password" class="input w-full"
placeholder="Enter your password">
<p x-show="passwordError" x-text="passwordError" class="text-red-500 text-sm mt-1"></p>
@error('password')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
</div>
</div>
<!-- Navigation buttons -->
<div class="flex flex-wrap gap-2 justify-between mt-4">
<template x-if="step > initialStep">
<x-forms.button @click="step--" class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">
Back
</x-forms.button> </x-forms.button>
@else </template>
<x-forms.button @click="modalOpen=false" class="w-24" isHighlighted type="button" <template x-if="step === initialStep">
wire:click.prevent="{{ $action }}">Continue <x-forms.button @click="modalOpen = false; resetModal()"
class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">
Cancel
</x-forms.button> </x-forms.button>
@endif </template>
@endif
<template x-if="step === 1">
<x-forms.button @click="step++" class="w-auto" isError>
<span x-text="step1ButtonText"></span>
</x-forms.button>
</template>
<template x-if="step === 2">
<x-forms.button x-bind:disabled="confirmWithText && userConfirmationText !== confirmationText"
class="w-auto" isError
@click="
if (dispatchEvent) {
$wire.dispatch(dispatchEventType, dispatchEventMessage);
}
if (confirmWithPassword) {
step++;
} else {
modalOpen = false;
resetModal();
submitForm();
}">
<span x-text="step2ButtonText"></span>
</x-forms.button>
</template>
<template x-if="step === 3 && confirmWithPassword">
<x-forms.button x-bind:disabled="!password" class="w-auto" isError
@click="
if (dispatchEvent) {
$wire.dispatch(dispatchEventType, dispatchEventMessage);
}
submitForm().then((result) => {
if (result === true) {
modalOpen = false;
resetModal();
} else {
passwordError = result;
}
});
">
<span x-text="step3ButtonText"></span>
</x-forms.button>
</template>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -122,9 +122,19 @@
@if (count($deployments_per_server) > 0) @if (count($deployments_per_server) > 0)
<x-loading /> <x-loading />
@endif @endif
<x-modal-confirmation isErrorButton action="cleanup_queue" buttonTitle="Cleanup Queues"> <x-modal-confirmation
This will clean up the deployment queue. <br>Please think again. title="Confirm Cleanup Queues?"
</x-modal-confirmation> buttonTitle="Cleanup Queues"
isErrorButton
submitAction="cleanup_queue"
:actions="['All running Deployment Queues will be cleaned up.']"
:confirmWithText="false"
:confirmWithPassword="false"
step2ButtonText="Permanently Cleanup Deployment Queues"
:dispatchEvent="true"
dispatchEventType="success"
dispatchEventMessage="Deployment Queues cleanup started."
/>
</div> </div>
<div wire:poll.3000ms="get_deployments" class="grid grid-cols-1"> <div wire:poll.3000ms="get_deployments" class="grid grid-cols-1">
@forelse ($deployments_per_server as $server_name => $deployments) @forelse ($deployments_per_server as $server_name => $deployments)
@@ -170,4 +180,6 @@
} }
</script> </script>
{{-- <x-forms.button wire:click='getIptables'>Get IPTABLES</x-forms.button> --}} {{-- <x-forms.button wire:click='getIptables'>Get IPTABLES</x-forms.button> --}}
</div> </div>

View File

@@ -6,9 +6,18 @@
Save Save
</x-forms.button> </x-forms.button>
@if ($destination->network !== 'coolify') @if ($destination->network !== 'coolify')
<x-modal-confirmation isErrorButton buttonTitle="Delete Destination"> <x-modal-confirmation
This destination will be deleted. It is not reversible. <br>Please think again. title="Confirm Destination Deletion?"
</x-modal-confirmation> buttonTitle="Delete Destination"
isErrorButton
submitAction="delete"
:actions="['This will delete the selected destination/network.']"
confirmationText="{{ $destination->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Destination Name below"
shortConfirmationLabel="Destination Name"
:confirmWithPassword="false"
step2ButtonText="Permanently Delete Destination"
/>
@endif @endif
</div> </div>

View File

@@ -1,5 +1,13 @@
<div> <div>
<x-modal-confirmation buttonFullWidth isErrorButton buttonTitle="Delete Team"> <x-modal-confirmation
This team be deleted. It is not reversible. <br>Please think again. title="Confirm Team Deletion?"
</x-modal-confirmation> buttonTitle="Delete Team"
isErrorButton
submitAction="delete"
:actions="['The current Team will be permanently deleted.']"
confirmationText="{{ $team }}"
confirmationLabel="Please confirm the execution of the actions by entering the Team Name below"
shortConfirmationLabel="Team Name"
step3ButtonText="Permanently Delete Team"
/>
</div> </div>

View File

@@ -68,11 +68,20 @@
<option value="www">Redirect to www.</option> <option value="www">Redirect to www.</option>
<option value="non-www">Redirect to non-www.</option> <option value="non-www">Redirect to non-www.</option>
</x-forms.select> </x-forms.select>
<x-modal-confirmation action="set_redirect"> <x-modal-confirmation
title="Confirm Redirection Setting?"
buttonTitle="Set Direction"
submitAction="set_redirect"
:actions="['All traffic will be redirected to the selected direction.']"
confirmationText="{{ $application->fqdn . '/' }}"
confirmationLabel="Please confirm the execution of the action by entering the Application URL below"
shortConfirmationLabel="Application URL"
:confirmWithPassword="false"
step2ButtonText="Set Direction"
>
<x-slot:customButton> <x-slot:customButton>
<div class="w-[7.2rem]">Set Direction</div> <div class="w-[7.2rem]">Set Direction</div>
</x-slot:customButton> </x-slot:customButton>
This will reset the container labels. Are you sure?
</x-modal-confirmation> </x-modal-confirmation>
</div> </div>
@endif @endif
@@ -302,12 +311,18 @@
helper="If you know what are you doing, you can enable this to edit the labels directly. Coolify won't update labels automatically. <br><br>Be careful, it could break the proxy configuration after you restart the container." helper="If you know what are you doing, you can enable this to edit the labels directly. Coolify won't update labels automatically. <br><br>Be careful, it could break the proxy configuration after you restart the container."
id="application.settings.is_container_label_readonly_enabled" instantSave></x-forms.checkbox> id="application.settings.is_container_label_readonly_enabled" instantSave></x-forms.checkbox>
</div> </div>
<x-modal-confirmation buttonFullWidth action="resetDefaultLabels" <x-modal-confirmation
buttonTitle="Reset to Coolify Generated Labels"> title="Confirm Labels Reset to Coolify Defaults?"
Are you sure you want to reset the labels to Coolify generated labels? <br>It could break the proxy buttonTitle="Reset Labels to Coolify Defaults"
configuration after you restart the container. buttonFullWidth
</x-modal-confirmation> submitAction="resetDefaultLabels"
:actions="['All your custom proxy labels will be lost.', 'Proxy labels (traefik, caddy, etc) will be reset to the coolify defaults.']"
confirmationText="{{ $application->fqdn . '/' }}"
confirmationLabel="Please confirm the execution of the actions by entering the Application URL below"
shortConfirmationLabel="Application URL"
:confirmWithPassword="false"
step2ButtonText="Permanently Reset Labels"
/>
@endif @endif
<h3 class="pt-8">Pre/Post Deployment Commands</h3> <h3 class="pt-8">Pre/Post Deployment Commands</h3>

View File

@@ -72,7 +72,13 @@
</x-forms.button> </x-forms.button>
@endif @endif
@endif @endif
<x-modal-confirmation @click="$wire.dispatch('stopEvent')"> <x-modal-confirmation title="Confirm Application Stopping?" buttonTitle="Stop"
submitAction="stop" :checkboxes="$checkboxes" :actions="[
'This application will be stopped.',
'All non-persistent data of this application will be deleted.',
]" :confirmWithText="false" :confirmWithPassword="false"
step1ButtonText="Continue" step2ButtonText="Confirm" :dispatchEvent="true"
dispatchEventType="stopEvent">
<x-slot:button-title> <x-slot:button-title>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
@@ -87,7 +93,6 @@
</svg> </svg>
Stop Stop
</x-slot:button-title> </x-slot:button-title>
This application will be stopped. <br>Please think again.
</x-modal-confirmation> </x-modal-confirmation>
@else @else
<x-forms.button wire:click='deploy'> <x-forms.button wire:click='deploy'>

View File

@@ -152,8 +152,15 @@
@endif @endif
</x-forms.button> </x-forms.button>
@if (data_get($preview, 'status') !== 'exited') @if (data_get($preview, 'status') !== 'exited')
<x-modal-confirmation isErrorButton <x-modal-confirmation
action="stop({{ data_get($preview, 'pull_request_id') }})"> title="Confirm Preview Deployment Stopping?"
buttonTitle="Stop"
submitAction="stop({{ data_get($preview, 'pull_request_id') }})"
:actions="['This preview deployment will be stopped.', 'If the preview deployment is currently in use data could be lost.', 'All non-persistent data of this preview deployment (containers, networks, unused images) will be deleted (don\'t worry, no data is lost and you can start the preview deployment again).']"
:confirmWithText="false"
:confirmWithPassword="false"
step2ButtonText="Stop Preview Deployment"
>
<x-slot:customButton> <x-slot:customButton>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error"
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
@@ -168,14 +175,19 @@
</svg> </svg>
Stop Stop
</x-slot:customButton> </x-slot:customButton>
This will stop the preview deployment. <br>Please think again.
</x-modal-confirmation> </x-modal-confirmation>
@endif @endif
<x-modal-confirmation isErrorButton <x-modal-confirmation
action="delete({{ data_get($preview, 'pull_request_id') }})" buttonTitle="Delete"> title="Confirm Preview Deployment Deletion?"
This will delete the preview deployment. <br>Please think again. buttonTitle="Delete"
</x-modal-confirmation> isErrorButton
submitAction="delete({{ data_get($preview, 'pull_request_id') }})"
:actions="['All containers of this preview deployment will be stopped and permanently deleted.']"
confirmationText="{{ data_get($preview, 'fqdn'). '/' }}"
confirmationLabel="Please confirm the execution of the actions by entering the Preview Deployment name below"
shortConfirmationLabel="Preview Deployment Name"
:confirmWithPassword="false"
/>
</div> </div>
</div> </div>
@endforeach @endforeach

View File

@@ -8,12 +8,17 @@
<livewire:project.database.backup-now :backup="$backup" /> <livewire:project.database.backup-now :backup="$backup" />
@endif @endif
@if ($backup->database_id !== 0) @if ($backup->database_id !== 0)
<x-modal-confirmation isErrorButton> <x-modal-confirmation
<x-slot:button-title> title="Confirm Backup Schedule Deletion?"
Delete buttonTitle="Delete Backups and Schedule"
</x-slot:button-title> isErrorButton
This will stop the scheduled backup for this database.<br>Please think again. submitAction="delete"
</x-modal-confirmation> :checkboxes="$checkboxes"
:actions="['The selected backup schedule will be deleted.', 'Scheduled backups for this database will be stopped (if this is the only backup schedule for this database).']"
confirmationText="{{ $backup->database->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Database Name of the scheduled backups below"
shortConfirmationLabel="Database Name"
/>
@endif @endif
</div> </div>
<div class="w-48 pb-2"> <div class="w-48 pb-2">

View File

@@ -45,12 +45,20 @@
<x-forms.button class="dark:hover:bg-coolgray-400" <x-forms.button class="dark:hover:bg-coolgray-400"
x-on:click="download_file('{{ data_get($execution, 'id') }}')">Download</x-forms.button> x-on:click="download_file('{{ data_get($execution, 'id') }}')">Download</x-forms.button>
@endif @endif
<x-modal-confirmation isErrorButton action="deleteBackup({{ data_get($execution, 'id') }})"> <x-modal-confirmation
<x-slot:button-title> title="Confirm Backup Deletion?"
Delete buttonTitle="Delete"
</x-slot:button-title> isErrorButton
This will delete this backup. It is not reversible.<br>Please think again. submitAction="deleteBackup({{ data_get($execution, 'id') }})"
</x-modal-confirmation> {{-- :checkboxes="$checkboxes" --}}
:actions="[
'This backup will be permanently deleted from local storage.'
]"
confirmationText="{{ data_get($execution, 'filename') }}"
confirmationLabel="Please confirm the execution of the actions by entering the Backup Filename below"
shortConfirmationLabel="Backup Filename"
step3ButtonText="Permanently Delete Backup"
/>
</div> </div>
</div> </div>
@empty @empty

View File

@@ -27,32 +27,49 @@
$database->getMorphClass() === 'App\Models\StandaloneMongodb' || $database->getMorphClass() === 'App\Models\StandaloneMongodb' ||
$database->getMorphClass() === 'App\Models\StandaloneMysql' || $database->getMorphClass() === 'App\Models\StandaloneMysql' ||
$database->getMorphClass() === 'App\Models\StandaloneMariadb') $database->getMorphClass() === 'App\Models\StandaloneMariadb')
<a class="{{ request()->routeIs('project.database.backup.index') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('project.database.backup.index') ? 'dark:text-white' : '' }}" href="{{ route('project.database.backup.index', $parameters) }}">
href="{{ route('project.database.backup.index', $parameters) }}">
<button>Backups</button> <button>Backups</button>
</a> </a>
@endif @endif
</nav> </nav>
<div class="flex flex-wrap gap-2 items-center"> <div class="flex flex-wrap gap-2 items-center">
@if (!str($database->status)->startsWith('exited')) @if (!str($database->status)->startsWith('exited'))
<x-modal-confirmation @click="$wire.dispatch('restartEvent')"> <x-modal-confirmation
title="Confirm Database Restart?"
buttonTitle="Restart"
submitAction="restart"
:actions="['This database will be unavailable during the restart.', 'If the database is currently in use data could be lost.']"
:confirmWithText="false"
:confirmWithPassword="false"
step2ButtonText="Restart Database"
:dispatchEvent="true"
dispatchEventType="restartEvent"
>
<x-slot:button-title> <x-slot:button-title>
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" /> <path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" /> <path d="M20 4v5h-5" />
</g> </g>
</svg> </svg>
Restart Restart
</x-slot:button-title> </x-slot:button-title>
This database will be restarted. <br>Please think again.
</x-modal-confirmation> </x-modal-confirmation>
<x-modal-confirmation @click="$wire.dispatch('stopEvent')"> <x-modal-confirmation
title="Confirm Database Stopping?"
buttonTitle="Stop"
submitAction="stop"
:checkboxes="$checkboxes"
:actions="['This database will be stopped.', 'If the database is currently in use data could be lost.', 'All non-persistent data of this database (containers, networks, unused images) will be deleted (don\'t worry, no data is lost and you can start the database again).']"
:confirmWithText="false"
:confirmWithPassword="false"
step1ButtonText="Continue Stopping Database"
step2ButtonText="Stop Database"
:dispatchEvent="true"
dispatchEventType="stopEvent"
>
<x-slot:button-title> <x-slot:button-title>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"> <path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path> </path>
@@ -61,13 +78,10 @@
</svg> </svg>
Stop Stop
</x-slot:button-title> </x-slot:button-title>
This database will be stopped. <br>Please think again.
</x-modal-confirmation> </x-modal-confirmation>
@else @else
<button @click="$wire.dispatch('startEvent')" class="gap-2 button"> <button @click="$wire.dispatch('startEvent')" class="gap-2 button">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" /> <path d="M7 4v16l13 -8z" />
</svg> </svg>

View File

@@ -2,9 +2,21 @@
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<x-forms.input id="filename" label="Filename" /> <x-forms.input id="filename" label="Filename" />
<x-forms.button type="submit">Save</x-forms.button> <x-forms.button type="submit">Save</x-forms.button>
<x-modal-confirmation isErrorButton buttonTitle="Delete"> <x-modal-confirmation
This script will be deleted. It is not reversible. <br>Please think again. title="Confirm init-script deletion?"
</x-modal-confirmation> buttonTitle="Delete"
isErrorButton
submitAction="delete"
:actions="[
'The init-script of this database will be permanently deleted.',
'If you are actively using this init-script, it could cause errors on redeployment.'
]"
confirmationText="{{ $filename }}"
confirmationLabel="Please confirm the execution of the actions by entering the init-script name below"
shortConfirmationLabel="Init-script Name"
:confirmWithPassword=false
step2ButtonText="Permanently Delete Init-script"
/>
</div> </div>
<x-forms.textarea id="content" label="Content" /> <x-forms.textarea id="content" label="Content" />
</form> </form>

View File

@@ -1,3 +1,12 @@
<x-modal-confirmation isErrorButton buttonTitle="Delete Environment" disabled="{{ $disabled }}"> <x-modal-confirmation
This environment will be deleted. It is not reversible. <br>Please think again. title="Confirm Environment Deletion?"
</x-modal-confirmation> buttonTitle="Delete Environment"
isErrorButton
submitAction="delete"
:actions="['This will delete the selected environment.']"
confirmationLabel="Please confirm the execution of the actions by entering the Environment Name below"
shortConfirmationLabel="Environment Name"
confirmationText="{{ $environmentName }}"
:confirmWithPassword="false"
step2ButtonText="Permanently Delete Environment"
/>

View File

@@ -1,3 +1,12 @@
<x-modal-confirmation isErrorButton buttonTitle="Delete Project" disabled="{{ $disabled }}"> <x-modal-confirmation
This project will be deleted. It is not reversible. <br>Please think again. title="Confirm Project Deletion?"
</x-modal-confirmation> buttonTitle="Delete Project"
isErrorButton
submitAction="delete"
:actions="['This will delete the selected project', 'All Environments inside the project will be deleted as well.']"
confirmationLabel="Please confirm the execution of the actions by entering the Project Name below"
shortConfirmationLabel="Project Name"
confirmationText="{{ $projectName }}"
:confirmWithPassword="false"
step2ButtonText="Permanently Delete Project"
/>

View File

@@ -107,11 +107,15 @@
Settings Settings
</a> </a>
@if (str($application->status)->contains('running')) @if (str($application->status)->contains('running'))
<x-modal-confirmation action="restartApplication({{ $application->id }})" <x-modal-confirmation
isErrorButton buttonTitle="Restart"> title="Confirm Service Application Restart?"
This application will be unavailable during the restart. <br>Please think buttonTitle="Restart"
again. submitAction="restartApplication({{ $application->id }})"
</x-modal-confirmation> :actions="['The selected service application will be unavailable during the restart.', 'If the service application is currently in use data could be lost.']"
:confirmWithText="false"
:confirmWithPassword="false"
step2ButtonText="Restart Service Container"
/>
@endif @endif
</div> </div>
</div> </div>
@@ -151,11 +155,15 @@
Settings Settings
</a> </a>
@if (str($database->status)->contains('running')) @if (str($database->status)->contains('running'))
<x-modal-confirmation action="restartDatabase({{ $database->id }})" <x-modal-confirmation
isErrorButton buttonTitle="Restart"> title="Confirm Service Database Restart?"
This database will be unavailable during the restart. <br>Please think buttonTitle="Restart"
again. submitAction="restartDatabase({{ $database->id }})"
</x-modal-confirmation> :actions="['This service database will be unavailable during the restart.', 'If the service database is currently in use data could be lost.']"
:confirmWithText="false"
:confirmWithPassword="false"
step2ButtonText="Restart Database"
/>
@endif @endif
</div> </div>
</div> </div>

View File

@@ -15,18 +15,56 @@
<form wire:submit='submit' class="flex flex-col gap-2"> <form wire:submit='submit' class="flex flex-col gap-2">
<div class="flex gap-2"> <div class="flex gap-2">
@if ($fileStorage->is_directory) @if ($fileStorage->is_directory)
<x-modal-confirmation action="convertToFile" buttonTitle="Convert to file"> <x-modal-confirmation
<div>This will delete all files in this directory. It is not reversible. <strong title="Confirm Directory Conversion to File?"
class="text-error">Please think buttonTitle="Convert to file"
again.</strong><br><br></div> submitAction="convertToFile"
</x-modal-confirmation> :actions="['All files in this directory will be permanently deleted and an empty file will be created in its place.']"
confirmationText="{{ $fs_path }}"
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
shortConfirmationLabel="Filepath"
:confirmWithPassword="false"
step2ButtonText="Convert to file"
/>
@else @else
<x-modal-confirmation action="convertToDirectory" buttonTitle="Convert to directory"> <x-modal-confirmation
<div>This will delete the file and make a directory instead. It is not reversible. title="Confirm File Conversion to Directory?"
<strong class="text-error">Please think buttonTitle="Convert to directory"
again.</strong><br><br> submitAction="convertToDirectory"
</div> :actions="['The selected file will be permanently deleted and an empty directory will be created in its place.']"
</x-modal-confirmation> confirmationText="{{ $fs_path }}"
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
shortConfirmationLabel="Filepath"
:confirmWithPassword="false"
step2ButtonText="Convert to directory"
/>
@endif
@if ($fileStorage->is_directory)
<x-modal-confirmation
title="Confirm Directory Deletion?"
buttonTitle="Delete Directory"
isErrorButton
submitAction="delete"
:checkboxes="$directoryDeletionCheckboxes"
:actions="['The selected directory and all its contents will be permanently deleted from the container.']"
confirmationText="{{ $fs_path }}"
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
shortConfirmationLabel="Filepath"
step3ButtonText="Permanently Delete Directory"
/>
@else
<x-modal-confirmation
title="Confirm File Deletion?"
buttonTitle="Delete File"
isErrorButton
submitAction="delete"
:checkboxes="$fileDeletionCheckboxes"
:actions="['The selected file will be permanently deleted from the container.']"
confirmationText="{{ $fs_path }}"
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
shortConfirmationLabel="Filepath"
step3ButtonText="Permanently Delete File"
/>
@endif @endif
@if (!$fileStorage->is_based_on_git) @if (!$fileStorage->is_based_on_git)

View File

@@ -32,7 +32,9 @@
</svg> </svg>
Pull Latest Images & Restart Pull Latest Images & Restart
</button> </button>
<x-modal-confirmation @click="$wire.dispatch('stopEvent')"> <x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" submitAction="stop"
:checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]" :confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue"
step2ButtonText="Stop Service" :dispatchEvent="true" dispatchEventType="stopEvent">
<x-slot:button-title> <x-slot:button-title>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
@@ -45,7 +47,6 @@
</svg> </svg>
Stop Stop
</x-slot:button-title> </x-slot:button-title>
This service will be stopped. <br>Please think again.
</x-modal-confirmation> </x-modal-confirmation>
@elseif (str($service->status())->contains('degraded')) @elseif (str($service->status())->contains('degraded'))
<button @click="$wire.dispatch('startEvent')" class="gap-2 button"> <button @click="$wire.dispatch('startEvent')" class="gap-2 button">
@@ -58,7 +59,10 @@
</svg> </svg>
Restart Degraded Services Restart Degraded Services
</button> </button>
<x-modal-confirmation @click="$wire.dispatch('stopEvent')"> <x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" submitAction="stop"
:checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]" :confirmWithText="false" :confirmWithPassword="false"
step1ButtonText="Continue" step2ButtonText="Stop Service" :dispatchEvent="true"
dispatchEventType="stopEvent">
<x-slot:button-title> <x-slot:button-title>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
@@ -71,7 +75,6 @@
</svg> </svg>
Stop Stop
</x-slot:button-title> </x-slot:button-title>
This service will be stopped. <br>Please think again.
</x-modal-confirmation> </x-modal-confirmation>
@elseif (str($service->status())->contains('exited')) @elseif (str($service->status())->contains('exited'))
<button wire:click='stop(true)' class="gap-2 button"> <button wire:click='stop(true)' class="gap-2 button">
@@ -92,7 +95,10 @@
Deploy Deploy
</button> </button>
@else @else
<x-modal-confirmation @click="$wire.dispatch('stopEvent')"> <x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" submitAction="stop"
:checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]" :confirmWithText="false" :confirmWithPassword="false"
step1ButtonText="Continue" step2ButtonText="Stop Service" :dispatchEvent="true"
dispatchEventType="stopEvent">
<x-slot:button-title> <x-slot:button-title>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
@@ -105,7 +111,6 @@
</svg> </svg>
Stop Stop
</x-slot:button-title> </x-slot:button-title>
This service will be stopped. <br>Please think again.
</x-modal-confirmation> </x-modal-confirmation>
<button @click="$wire.dispatch('startEvent')" class="gap-2 button"> <button @click="$wire.dispatch('startEvent')" class="gap-2 button">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"

View File

@@ -7,12 +7,20 @@
<h2>{{ Str::headline($application->name) }}</h2> <h2>{{ Str::headline($application->name) }}</h2>
@endif @endif
<x-forms.button type="submit">Save</x-forms.button> <x-forms.button type="submit">Save</x-forms.button>
<x-modal-confirmation isErrorButton> <x-modal-confirmation
<x-slot:button-title> title="Confirm Service Application Deletion?"
Delete buttonTitle="Delete"
</x-slot:button-title> isErrorButton
This will delete this service application. It is not reversible.<br>Please think again. submitAction="delete"
</x-modal-confirmation> {{-- :checkboxes="$checkboxes" --}}
:actions="[
'The selected service application container will be stopped and permanently deleted.'
]"
confirmationText="{{ Str::headline($application->name) }}"
confirmationLabel="Please confirm the execution of the actions by entering the Service Application Name below"
shortConfirmationLabel="Service Application Name"
step3ButtonText="Permanently Delete Service Application"
/>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex gap-2"> <div class="flex gap-2">

View File

@@ -2,15 +2,10 @@
<h2>Danger Zone</h2> <h2>Danger Zone</h2>
<div class="">Woah. I hope you know what are you doing.</div> <div class="">Woah. I hope you know what are you doing.</div>
<h4 class="pt-4">Delete Resource</h4> <h4 class="pt-4">Delete Resource</h4>
<div class="pb-4">This will stop your containers, delete all related data, etc. Beware! There is no coming <div class="pb-4">This will stop your containers, delete all related data, etc. Beware! There is no coming back!
back!
</div> </div>
<x-modal-confirmation isErrorButton buttonTitle="Delete" confirm={{ $confirm }}> <x-modal-confirmation title="Confirm Resource Deletion?" buttonTitle="Delete" isErrorButton submitAction="delete"
<div class="px-2">This resource will be deleted. It is not reversible. <strong class="text-error">Please think buttonTitle="Delete" :checkboxes="$checkboxes" :actions="['All containers of this resource will be stopped and permanently deleted.']" confirmationText="{{ $resourceName }}"
again.</strong><br><br></div> confirmationLabel="Please confirm the execution of the actions by entering the NAME of the resource below"
<h4>Actions</h4> shortConfirmationLabel="Resource Name" step3ButtonText="Delete Permanently" />
<x-forms.checkbox id="delete_configurations"
label="Permanently delete configuration files from the server?"></x-forms.checkbox>
<x-forms.checkbox id="delete_volumes" label="Permanently delete associated volumes?"></x-forms.checkbox>
</x-modal-confirmation>
</div> </div>

View File

@@ -63,11 +63,16 @@
wire:click="stop('{{ data_get($destination, 'server.id') }}')">Stop</x-forms.button> wire:click="stop('{{ data_get($destination, 'server.id') }}')">Stop</x-forms.button>
@endif @endif
<x-modal-confirmation <x-modal-confirmation
action="removeServer({{ data_get($destination, 'id') }},{{ data_get($destination, 'server.id') }})" title="Confirm server removal?"
isErrorButton buttonTitle="Remove Server"> isErrorButton
This will stop the running application in this server and remove it as a deployment buttonTitle="Remove Server"
destination.<br><br>Please think again. submitAction="removeServer({{ data_get($destination, 'id') }},{{ data_get($destination, 'server.id') }})"
</x-modal-confirmation> :actions="['This will stop the all running applications on this server and remove it as a deployment destination.']"
confirmationText="{{ data_get($destination, 'server.name') }}"
confirmationLabel="Please confirm the execution of the actions by entering the Server Name below"
shortConfirmationLabel="Server Name"
step3ButtonText="Permanently Remove Server"
/>
</div> </div>
</div> </div>
@endforeach @endforeach

View File

@@ -11,10 +11,20 @@
<path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0-2 0m-3-5V7a4 4 0 1 1 8 0v4" /> <path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0-2 0m-3-5V7a4 4 0 1 1 8 0v4" />
</g> </g>
</svg> </svg>
<x-modal-confirmation isErrorButton buttonTitle="Delete"> <x-modal-confirmation
You will delete environment variable <span title="Confirm Environment Variable Deletion?"
class="font-bold dark:text-warning text-coollabs">{{ $env->key }}</span>. isErrorButton
</x-modal-confirmation> buttonTitle="Delete"
submitAction="delete"
:actions="[
'The selected environment variable will be permanently deleted.'
]"
confirmationText="{{ $env->key }}"
confirmationLabel="Please confirm the execution of the actions by entering the Environment Variable Name below"
shortConfirmationLabel="Environment Variable Name"
:confirmWithPassword="false"
step2ButtonText="Permanently Delete Environment Variable"
/>
</div> </div>
@else @else
@if ($isDisabled) @if ($isDisabled)
@@ -70,10 +80,20 @@
<x-forms.button wire:click='lock'> <x-forms.button wire:click='lock'>
Lock Lock
</x-forms.button> </x-forms.button>
<x-modal-confirmation isErrorButton buttonTitle="Delete"> <x-modal-confirmation
You will delete environment variable <span title="Confirm Environment Variable Deletion?"
class="font-bold dark:text-warning">{{ $env->key }}</span>. isErrorButton
</x-modal-confirmation> buttonTitle="Delete"
submitAction="delete"
:actions="[
'The selected environment variable will be permanently deleted.'
]"
confirmationText="{{ $env->key }}"
confirmationLabel="Please confirm the execution of the actions by entering the Environment Variable Name below"
shortConfirmationLabel="Environment Variable Name"
:confirmWithPassword="false"
step2ButtonText="Permanently Delete Environment Variable"
/>
@else @else
<x-forms.button type="submit"> <x-forms.button type="submit">
Update Update
@@ -81,10 +101,20 @@
<x-forms.button wire:click='lock'> <x-forms.button wire:click='lock'>
Lock Lock
</x-forms.button> </x-forms.button>
<x-modal-confirmation buttonFullWidth isErrorButton buttonTitle="Delete"> <x-modal-confirmation
You will delete environment variable <span title="Confirm Environment Variable Deletion?"
class="font-bold dark:text-warning">{{ $env->key }}</span>. isErrorButton
</x-modal-confirmation> buttonTitle="Delete"
submitAction="delete"
:actions="[
'The selected environment variable will be permanently deleted.'
]"
confirmationText="{{ $env->key }}"
confirmationLabel="Please confirm the execution of the actions by entering the Environment Variable Name below"
shortConfirmationLabel="Environment Variable Name"
:confirmWithPassword="false"
step2ButtonText="Permanently Delete Environment Variable"
/>
@endif @endif
</div> </div>
@endif @endif

View File

@@ -8,7 +8,20 @@
@foreach ($servers->sortBy('id') as $server) @foreach ($servers->sortBy('id') as $server)
<h5>Server: <span class="font-bold text-dark dark:text-white">{{ $server->name }}</span></h5> <h5>Server: <span class="font-bold text-dark dark:text-white">{{ $server->name }}</span></h5>
@foreach ($server->destinations() as $destination) @foreach ($server->destinations() as $destination)
<x-modal-confirmation action="cloneTo({{ data_get($destination, 'id') }})"> <x-modal-confirmation
title="Clone Resource?"
buttonTitle="Clone Resource"
submitAction="cloneTo({{ data_get($destination, 'id') }})"
:actions="[
'All containers of this resource will be duplicated and cloned to the selected destination.'
]"
:confirmWithText="false"
:confirmWithPassword="false"
step2ButtonText="Clone Resource"
dispatchEvent="true"
dispatchEventType="success"
dispatchEventMessage="Resource cloned to {{ $destination->name }} destination."
>
<x:slot name="content"> <x:slot name="content">
<div class="box group"> <div class="box group">
<div class="flex flex-col"> <div class="flex flex-col">
@@ -17,7 +30,6 @@
</div> </div>
</div> </div>
</x:slot> </x:slot>
<div>You are about to clone this resource.</div>
</x-modal-confirmation> </x-modal-confirmation>
@endforeach @endforeach
@endforeach @endforeach
@@ -36,8 +48,21 @@
<h5>Project: <span class="font-bold text-dark dark:text-white">{{ $project->name }}</span></h5> <h5>Project: <span class="font-bold text-dark dark:text-white">{{ $project->name }}</span></h5>
@foreach ($project->environments as $environment) @foreach ($project->environments as $environment)
<x-modal-confirmation action="moveTo({{ data_get($environment, 'id') }})"> <x-modal-confirmation
<x:slot name="content"> title="Move Resource?"
buttonTitle="Move Resource"
submitAction="moveTo({{ data_get($environment, 'id') }})"
:actions="[
'All containers of this resource will be moved to the selected environment.'
]"
:confirmWithText="false"
:confirmWithPassword="false"
step2ButtonText="Move Resource"
dispatchEvent="true"
dispatchEventType="success"
dispatchEventMessage="Resource moved to {{ $environment->name }} environment."
>
<x:slot:content>
<div class="box group"> <div class="box group">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="box-title">Environment</div> <div class="box-title">Environment</div>
@@ -45,7 +70,6 @@
</div> </div>
</div> </div>
</x:slot> </x:slot>
<div>You are about to move this resource.</div>
</x-modal-confirmation> </x-modal-confirmation>
@endforeach @endforeach
@empty @empty

View File

@@ -16,13 +16,19 @@
<x-forms.button type="submit"> <x-forms.button type="submit">
Save Save
</x-forms.button> </x-forms.button>
<x-modal-confirmation isErrorButton buttonTitle="Delete Scheduled Task"> <x-modal-confirmation
You will delete scheduled task <span class="font-bold dark:text-warning">{{ $task->name }}</span>. title="Confirm Scheduled Task Deletion?"
</x-modal-confirmation> isErrorButton
</div> buttonTitle="Delete"
<div class="w-48"> submitAction="delete({{ $task->id }})"
<x-forms.checkbox instantSave id="task.enabled" label="Enabled" /> :actions="['The selected scheduled task will be permanently deleted.']"
</div> confirmationText="{{ $task->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Scheduled Task Name below"
shortConfirmationLabel="Scheduled Task Name"
:confirmWithPassword="false"
step2ButtonText="Permanently Delete Scheduled Task"
/>
</div> </div>
<div class="flex w-full gap-2"> <div class="flex w-full gap-2">
<x-forms.input placeholder="Name" id="task.name" label="Name" required /> <x-forms.input placeholder="Name" id="task.name" label="Name" required />

View File

@@ -46,13 +46,17 @@
<x-forms.button type="submit"> <x-forms.button type="submit">
Update Update
</x-forms.button> </x-forms.button>
<x-modal-confirmation isErrorButton buttonTitle="Delete"> <x-modal-confirmation
This storage will be deleted <span class="font-bold dark:text-warning">{{ $storage->name }}</span>. title="Confirm persistent storage deletion?"
It isErrorButton
is buttonTitle="Delete"
not submitAction="delete"
reversible. <br>Please think again. :actions="['The selected persistent storage/volume will be permanently deleted.', 'If the persistent storage/volume is actvily used by a resource data will be lost.']"
</x-modal-confirmation> confirmationText="{{ $storage->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Storage Name below"
shortConfirmationLabel="Storage Name"
step3ButtonText="Permanently Delete Persistent Storage/Volume"
/>
</div> </div>
@endif @endif
</form> </form>

View File

@@ -56,12 +56,18 @@
@endif @endif
</div> </div>
<x-modal-confirmation isErrorButton action="revoke({{ data_get($token, 'id') }})"> <x-modal-confirmation
<x-slot:button-title> title="Confirm API Token Revocation?"
Revoke token isErrorButton
</x-slot:button-title> buttonTitle="Revoke token"
This API Token will be deleted and anything using it will fail. <br>Please think again. submitAction="revoke({{ data_get($token, 'id') }})"
</x-modal-confirmation> :actions="['This API Token will be revoked and permanently deleted.', 'Any API call made with this token will fail.']"
confirmationText="{{ $token->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the API Token Description below"
shortConfirmationLabel="API Token Description"
:confirmWithPassword="false"
step2ButtonText="Revoke API Token"
/>
</div> </div>
@empty @empty
<div> <div>

View File

@@ -11,9 +11,18 @@
Save Save
</x-forms.button> </x-forms.button>
@if (data_get($private_key, 'id') > 0) @if (data_get($private_key, 'id') > 0)
<x-modal-confirmation isErrorButton buttonTitle="Delete"> <x-modal-confirmation
This private key will be deleted. It is not reversible. <br>Please think again. title="Confirm Private Key Deletion?"
</x-modal-confirmation> isErrorButton
buttonTitle="Delete"
submitAction="delete({{ $private_key->id }})"
:actions="['This private key will be permanently deleted.', 'All servers connected to this private key will stop working.', 'Any git app using this private key will stop working.']"
confirmationText="{{ $private_key->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Private Key Name below"
shortConfirmationLabel="Private Key Name"
:confirmWithPassword="false"
step2ButtonText="Delete Private Key"
/>
@endif @endif
</div> </div>
<x-forms.input id="private_key.name" label="Name" required /> <x-forms.input id="private_key.name" label="Name" required />

View File

@@ -8,13 +8,29 @@
</div> </div>
@if ($server->definedResources()->count() > 0) @if ($server->definedResources()->count() > 0)
<div class="pb-2 text-red-500">You need to delete all resources before deleting this server.</div> <div class="pb-2 text-red-500">You need to delete all resources before deleting this server.</div>
<x-modal-confirmation disabled isErrorButton buttonTitle="Delete"> <x-modal-confirmation
This server will be deleted. It is not reversible. <br>Please think again. title="Confirm Server Deletion?"
</x-modal-confirmation> isErrorButton
buttonTitle="Delete"
submitAction="delete"
:actions="['This server will be permanently deleted.']"
confirmationText="{{ $server->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Server Name below"
shortConfirmationLabel="Server Name"
step3ButtonText="Permanently Delete Server"
/>
@else @else
<x-modal-confirmation isErrorButton buttonTitle="Delete"> <x-modal-confirmation
This server will be deleted. It is not reversible. <br>Please think again. title="Confirm Server Deletion?"
</x-modal-confirmation> isErrorButton
buttonTitle="Delete"
submitAction="delete"
:actions="['This server will be permanently deleted.']"
confirmationText="{{ $server->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Server Name below"
shortConfirmationLabel="Server Name"
step2ButtonText="Permanently Delete Server"
/>
@endif @endif
@endif @endif
</div> </div>

View File

@@ -3,11 +3,15 @@
<div class="flex gap-2"> <div class="flex gap-2">
<h2>General</h2> <h2>General</h2>
@if ($server->id === 0) @if ($server->id === 0)
<x-modal-confirmation buttonTitle="Save" title="Change Localhost" action="submit"> <x-modal-confirmation
You could lose a lot of functionalities if you change the server details of the server where Coolify title="Confirm Server Settings Change?"
is buttonTitle="Save"
running on.<br>Please think again. submitAction="submit"
</x-modal-confirmation> :actions="['You could lose a lot of functionalities if you change the server details of the server where Coolify is running on.']"
:confirmWithText="false"
:confirmWithPassword="false"
step2ButtonText="Save Server Settings"
/>
@else @else
<x-forms.button type="submit">Save</x-forms.button> <x-forms.button type="submit">Save</x-forms.button>
@if ($server->isFunctional()) @if ($server->isFunctional())

View File

@@ -24,7 +24,17 @@
</a> </a>
</button> </button>
@endif @endif
<x-modal-confirmation @click="$wire.dispatch('restartEvent')"> <x-modal-confirmation
title="Confirm Proxy Restart?"
buttonTitle="Restart Proxy"
submitAction="restart"
:actions="['This proxy will be stopped and started again.', 'All resources hosted on coolify will be unavailable during the restart.']"
:confirmWithText="false"
:confirmWithPassword="false"
step2ButtonText="Restart Proxy"
:dispatchEvent="true"
dispatchEventType="restartEvent"
>
<x-slot:button-title> <x-slot:button-title>
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
@@ -35,10 +45,18 @@
</svg> </svg>
Restart Proxy Restart Proxy
</x-slot:button-title> </x-slot:button-title>
This proxy will be stopped and started. It is not reversible. <br>All resources will be unavailable
during the restart. <br>Please think again.
</x-modal-confirmation> </x-modal-confirmation>
<x-modal-confirmation @click="$wire.dispatch('stopEvent')"> <x-modal-confirmation
title="Confirm Proxy Stopping?"
buttonTitle="Stop Proxy"
submitAction="stop(true)"
:actions="['The coolify proxy will be stopped.', 'All resources hosted on coolify will be unavailable.']"
:confirmWithText="false"
:confirmWithPassword="false"
step2ButtonText="Stop Proxy"
:dispatchEvent="true"
dispatchEventType="stopEvent"
>
<x-slot:button-title> <x-slot:button-title>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
@@ -51,8 +69,6 @@
</svg> </svg>
Stop Proxy Stop Proxy
</x-slot:button-title> </x-slot:button-title>
This proxy will be stopped. It is not reversible. <br>All resources will be unavailable.
<br>Please think again.
</x-modal-confirmation> </x-modal-confirmation>
</div> </div>
@else @else

View File

@@ -14,13 +14,31 @@
</a> </a>
@endif @endif
@if ($applications->count() > 0) @if ($applications->count() > 0)
<x-modal-confirmation disabled isErrorButton buttonTitle="Delete"> <x-modal-confirmation
This source will be deleted. It is not reversible. <br>Please think again. title="Confirm GitHub App Deletion?"
</x-modal-confirmation> isErrorButton
buttonTitle="Delete"
submitAction="delete"
:actions="['The selected GitHub App will be permanently deleted.']"
confirmationText="{{ data_get($github_app, 'name') }}"
confirmationLabel="Please confirm the execution of the actions by entering the GitHub App Name below"
shortConfirmationLabel="GitHub App Name"
:confirmWithPassword="false"
step2ButtonText="Permanently Delete GitHub App"
/>
@else @else
<x-modal-confirmation isErrorButton buttonTitle="Delete"> <x-modal-confirmation
This source will be deleted. It is not reversible. <br>Please think again. title="Confirm GitHub App Deletion?"
</x-modal-confirmation> isErrorButton
buttonTitle="Delete"
submitAction="delete"
:actions="['The selected GitHub App will be permanently deleted.']"
confirmationLabel="Please confirm the execution of the actions by entering the GitHub App Name below"
shortConfirmationLabel="GitHub App Name"
confirmationText="{{ data_get($github_app, 'name') }}"
:confirmWithPassword="false"
step2ButtonText="Permanently Delete GitHub App"
/>
@endif @endif
</div> </div>
</div> </div>
@@ -160,9 +178,18 @@
<div class="flex items-center gap-2 pb-4"> <div class="flex items-center gap-2 pb-4">
<h1>GitHub App</h1> <h1>GitHub App</h1>
<div class="flex gap-2"> <div class="flex gap-2">
<x-modal-confirmation isErrorButton buttonTitle="Delete"> <x-modal-confirmation
This source will be deleted. It is not reversible. <br>Please think again. title="Confirm GitHub App Deletion?"
</x-modal-confirmation> isErrorButton
buttonTitle="Delete"
submitAction="delete"
:actions="['The selected GitHub App will be permanently deleted.']"
confirmationText="{{ data_get($github_app, 'name') }}"
confirmationLabel="Please confirm the execution of the actions by entering the GitHub App Name below"
shortConfirmationLabel="GitHub App Name"
:confirmWithPassword="false"
step2ButtonText="Permanently Delete GitHub App"
/>
</div> </div>
</div> </div>
<div class="mb-10 rounded alert-error"> <div class="mb-10 rounded alert-error">

View File

@@ -16,9 +16,18 @@
<x-forms.button wire:click="test_s3_connection"> <x-forms.button wire:click="test_s3_connection">
Validate Connection Validate Connection
</x-forms.button> </x-forms.button>
<x-modal-confirmation isErrorButton buttonTitle="Delete"> <x-modal-confirmation
This storage will be deleted. It is not reversible. Your data won't be touched!<br>Please think again. title="Confirm Storage Deletion?"
</x-modal-confirmation> isErrorButton
buttonTitle="Delete"
submitAction="delete({{ $storage->id }})"
:actions="['The selected storage location will be permanently deleted from Coolify.', 'If the storage location is in use by any backup jobs those backup jobs will only store the backup locally on the server.']"
confirmationText="{{ $storage->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Storage Name below"
shortConfirmationLabel="Storage Name"
:confirmWithPassword="false"
step2ButtonText="Permanently Delete Storage"
/>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input label="Name" id="storage.name" /> <x-forms.input label="Name" id="storage.name" />

View File

@@ -20,9 +20,18 @@
<div class="w-[500px]"> <div class="w-[500px]">
<x-forms.input readonly label="Deploy Webhook URL" id="webhook" /> <x-forms.input readonly label="Deploy Webhook URL" id="webhook" />
</div> </div>
<x-modal-confirmation isHighlighted buttonTitle="Redeploy All" action="redeploy_all"> <x-modal-confirmation
All resources will be redeployed. title="Redeploy all resources with this tag?"
</x-modal-confirmation> isHighlighted
buttonTitle="Redeploy All"
submitAction="redeploy_all"
:actions="['All resources with this tag will be redeployed.', 'During redeploy resources will be temporarily unavailable.']"
confirmationText="{{ $oneTag->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Tag Name below"
shortConfirmationLabel="Tag Name"
:confirmWithPassword="false"
step2ButtonText="Redeploy All"
/>
</div> </div>
<div class="grid grid-cols-1 gap-2 pt-4 lg:grid-cols-2 xl:grid-cols-3"> <div class="grid grid-cols-1 gap-2 pt-4 lg:grid-cols-2 xl:grid-cols-3">
@foreach ($applications as $application) @foreach ($applications as $application)

View File

@@ -24,9 +24,18 @@
<div class="w-[500px]"> <div class="w-[500px]">
<x-forms.input readonly label="Deploy Webhook URL" id="webhook" /> <x-forms.input readonly label="Deploy Webhook URL" id="webhook" />
</div> </div>
<x-modal-confirmation isHighlighted buttonTitle="Redeploy All" action="redeploy_all"> <x-modal-confirmation
All resources will be redeployed. title="Redeploy all resources with this tag?"
</x-modal-confirmation> isHighlighted
buttonTitle="Redeploy All"
submitAction="redeploy_all"
:actions="['All resources with this tag will be redeployed.', 'During redeploy resources will be temporarily unavailable.']"
confirmationText="{{ $oneTag->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Tag Name below"
shortConfirmationLabel="Tag Name"
:confirmWithPassword="false"
step2ButtonText="Redeploy All"
/>
</div> </div>
<div class="grid grid-cols-1 gap-2 pt-4 lg:grid-cols-2 xl:grid-cols-3"> <div class="grid grid-cols-1 gap-2 pt-4 lg:grid-cols-2 xl:grid-cols-3">
@foreach ($applications as $application) @foreach ($applications as $application)

View File

@@ -15,14 +15,17 @@
<div>{{ $user->email }}</div> <div>{{ $user->email }}</div>
<div class="flex-1"></div> <div class="flex-1"></div>
<div class="flex items-center justify-center gap-2 mx-4 text-xs font-bold "> <div class="flex items-center justify-center gap-2 mx-4 text-xs font-bold ">
<x-modal-confirmation isErrorButton action="delete({{ $user->id }})" buttonTitle="Delete"> <x-modal-confirmation
This will delete all resources (application, databases, services, configurations, servers, title="Confirm User Deletion?"
private keys, tags, etc.) from Coolify and <span buttonTitle="Delete"
class="font-bold text-red-500 dark:text-warning">from the server (if it's reachable)</span>. isErrorButton
<br> <br> submitAction="delete({{ $user->id }})"
It is not reversible. <br><br> :actions="['The selected user will be permanently deleted from Coolify and the database.', 'All resources (application, databases, services, configurations, servers, private keys, tags, etc.) related to this user will be deleted from Coolify and from the server (if the server is reachable).']"
<div class="font-bold text-red-500 dark:text-white">Think twice!</div> confirmationText="{{ $user->name }}"
</x-modal-confirmation> confirmationLabel="Please confirm the execution of the actions by entering the User Name below"
shortConfirmationLabel="User Name"
step3ButtonText="Permanently Delete User"
/>
</div> </div>
</div> </div>
@empty @empty

View File

@@ -31,9 +31,18 @@
@else @else
@if (currentTeam()->isEmpty()) @if (currentTeam()->isEmpty())
<div class="pb-4">This will delete your team. Beware! There is no coming back!</div> <div class="pb-4">This will delete your team. Beware! There is no coming back!</div>
<x-modal-confirmation isErrorButton buttonTitle="Delete"> <x-modal-confirmation
This team be deleted. It is not reversible. <br>Please think again. title="Confirm Team Deletion?"
</x-modal-confirmation> buttonTitle="Delete"
isErrorButton
submitAction="delete({{ currentTeam()->id }})"
:actions="['The current team will be permanently deleted from Coolify and the database.']"
confirmationText="{{ currentTeam()->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Team Name below"
shortConfirmationLabel="Team Name"
:confirmWithPassword="false"
step2ButtonText="Permanently Delete Team"
/>
@else @else
<div> <div>
<div class="pb-4">You need to delete the following resources to be able to delete the team:</div> <div class="pb-4">You need to delete the following resources to be able to delete the team:</div>

View File

@@ -1,10 +1,10 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.341" "version": "4.0.0-beta.342"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.342" "version": "4.0.0-beta.343"
}, },
"helper": { "helper": {
"version": "1.0.1" "version": "1.0.1"