diff --git a/app/Http/Controllers/UploadController.php b/app/Http/Controllers/UploadController.php index 4d34a1000..93847589a 100644 --- a/app/Http/Controllers/UploadController.php +++ b/app/Http/Controllers/UploadController.php @@ -13,7 +13,8 @@ class UploadController extends BaseController { public function upload(Request $request) { - $resource = getResourceByUuid(request()->route('databaseUuid'), data_get(auth()->user()->currentTeam(), 'id')); + $databaseIdentifier = request()->route('databaseUuid'); + $resource = getResourceByUuid($databaseIdentifier, data_get(auth()->user()->currentTeam(), 'id')); if (is_null($resource)) { return response()->json(['error' => 'You do not have permission for this database'], 500); } @@ -26,7 +27,10 @@ public function upload(Request $request) $save = $receiver->receive(); if ($save->isFinished()) { - return $this->saveFile($save->getFile(), $resource); + // Use the original identifier from the route to maintain path consistency + // For ServiceDatabase: {name}-{service_uuid} + // For standalone databases: {uuid} + return $this->saveFile($save->getFile(), $databaseIdentifier); } $handler = $save->handler(); @@ -57,10 +61,10 @@ public function upload(Request $request) // 'mime_type' => $mime // ]); // } - protected function saveFile(UploadedFile $file, $resource) + protected function saveFile(UploadedFile $file, string $resourceIdentifier) { $mime = str_replace('/', '-', $file->getMimeType()); - $filePath = "upload/{$resource->uuid}"; + $filePath = "upload/{$resourceIdentifier}"; $finalPath = storage_path('app/'.$filePath); $file->move($finalPath, 'restore'); diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index d70c52411..35262d7b0 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -8,7 +8,6 @@ use Livewire\Attributes\Locked; use Livewire\Attributes\Validate; use Livewire\Component; -use Spatie\Url\Url; class BackupEdit extends Component { @@ -184,13 +183,14 @@ public function delete($password) $this->backup->delete(); if ($this->backup->database->getMorphClass() === \App\Models\ServiceDatabase::class) { - $previousUrl = url()->previous(); - $url = Url::fromString($previousUrl); - $url = $url->withoutQueryParameter('selectedBackupId'); - $url = $url->withFragment('backups'); - $url = $url->getPath()."#{$url->getFragment()}"; + $serviceDatabase = $this->backup->database; - return redirect($url); + return redirect()->route('project.service.database.backups', [ + 'project_uuid' => $this->parameters['project_uuid'], + 'environment_uuid' => $this->parameters['environment_uuid'], + 'service_uuid' => $serviceDatabase->service->uuid, + 'stack_service_uuid' => $serviceDatabase->uuid, + ]); } else { return redirect()->route('project.database.backup.index', $this->parameters); } diff --git a/app/Livewire/Project/Database/Import.php b/app/Livewire/Project/Database/Import.php index 0b6e31ea0..2077ad66c 100644 --- a/app/Livewire/Project/Database/Import.php +++ b/app/Livewire/Project/Database/Import.php @@ -4,9 +4,11 @@ use App\Models\S3Storage; use App\Models\Server; +use App\Models\Service; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Storage; +use Livewire\Attributes\Computed; use Livewire\Component; class Import extends Component @@ -101,11 +103,23 @@ private function validateServerPath(string $path): bool public bool $unsupported = false; - public $resource; + // Store IDs instead of models for proper Livewire serialization + public ?int $resourceId = null; - public $parameters; + public ?string $resourceType = null; - public $containers; + public ?int $serverId = null; + + // View-friendly properties to avoid computed property access in Blade + public string $resourceUuid = ''; + + public string $resourceStatus = ''; + + public string $resourceDbType = ''; + + public array $parameters = []; + + public array $containers = []; public bool $scpInProgress = false; @@ -121,8 +135,6 @@ private function validateServerPath(string $path): bool public bool $error = false; - public Server $server; - public string $container; public array $importCommands = []; @@ -144,7 +156,7 @@ private function validateServerPath(string $path): bool public string $mongodbRestoreCommand = 'mongorestore --authenticationDatabase=admin --username $MONGO_INITDB_ROOT_USERNAME --password $MONGO_INITDB_ROOT_PASSWORD --uri mongodb://localhost:27017 --gzip --archive='; // S3 Restore properties - public $availableS3Storages = []; + public array $availableS3Storages = []; public ?int $s3StorageId = null; @@ -152,6 +164,26 @@ private function validateServerPath(string $path): bool public ?int $s3FileSize = null; + #[Computed] + public function resource() + { + if ($this->resourceId === null || $this->resourceType === null) { + return null; + } + + return $this->resourceType::find($this->resourceId); + } + + #[Computed] + public function server() + { + if ($this->serverId === null) { + return null; + } + + return Server::find($this->serverId); + } + public function getListeners() { $userId = Auth::id(); @@ -177,7 +209,7 @@ public function mount() public function updatedDumpAll($value) { $morphClass = $this->resource->getMorphClass(); - + // Handle ServiceDatabase by checking the database type if ($morphClass === \App\Models\ServiceDatabase::class) { $dbType = $this->resource->databaseType(); @@ -189,7 +221,7 @@ public function updatedDumpAll($value) $morphClass = 'postgresql'; } } - + switch ($morphClass) { case \App\Models\StandaloneMariadb::class: case 'mariadb': @@ -242,42 +274,95 @@ public function updatedDumpAll($value) public function getContainers() { - $this->containers = collect(); - if (! data_get($this->parameters, 'database_uuid')) { - abort(404); - } - $resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id')); - if (is_null($resource)) { - abort(404); - } - $this->authorize('view', $resource); - $this->resource = $resource; - $this->server = $this->resource->destination->server; - - // Handle ServiceDatabase container naming - if ($this->resource->getMorphClass() === \App\Models\ServiceDatabase::class) { - $this->container = $this->resource->name . '-' . $this->resource->service->uuid; + $this->containers = []; + $teamId = data_get(auth()->user()->currentTeam(), 'id'); + + // Try to find resource by route parameter + $databaseUuid = data_get($this->parameters, 'database_uuid'); + $stackServiceUuid = data_get($this->parameters, 'stack_service_uuid'); + + $resource = null; + if ($databaseUuid) { + // Standalone database route + $resource = getResourceByUuid($databaseUuid, $teamId); + if (is_null($resource)) { + abort(404); + } + } elseif ($stackServiceUuid) { + // ServiceDatabase route - look up the service database + $serviceUuid = data_get($this->parameters, 'service_uuid'); + $service = Service::whereUuid($serviceUuid)->first(); + if (! $service) { + abort(404); + } + $resource = $service->databases()->whereUuid($stackServiceUuid)->first(); + if (is_null($resource)) { + abort(404); + } } else { - $this->container = $this->resource->uuid; + abort(404); } - - if (str(data_get($this, 'resource.status'))->startsWith('running')) { - $this->containers->push($this->container); + + $this->authorize('view', $resource); + + // Store IDs for Livewire serialization + $this->resourceId = $resource->id; + $this->resourceType = get_class($resource); + + // Store view-friendly properties + $this->resourceStatus = $resource->status ?? ''; + + // Handle ServiceDatabase server access differently + if ($resource->getMorphClass() === \App\Models\ServiceDatabase::class) { + $server = $resource->service?->server; + if (! $server) { + abort(404, 'Server not found for this service database.'); + } + $this->serverId = $server->id; + $this->container = $resource->name.'-'.$resource->service->uuid; + $this->resourceUuid = $resource->uuid; // Use ServiceDatabase's own UUID + + // Determine database type for ServiceDatabase + $dbType = $resource->databaseType(); + if (str_contains($dbType, 'postgres')) { + $this->resourceDbType = 'standalone-postgresql'; + } elseif (str_contains($dbType, 'mysql')) { + $this->resourceDbType = 'standalone-mysql'; + } elseif (str_contains($dbType, 'mariadb')) { + $this->resourceDbType = 'standalone-mariadb'; + } elseif (str_contains($dbType, 'mongo')) { + $this->resourceDbType = 'standalone-mongodb'; + } else { + $this->resourceDbType = $dbType; + } + } else { + $server = $resource->destination?->server; + if (! $server) { + abort(404, 'Server not found for this database.'); + } + $this->serverId = $server->id; + $this->container = $resource->uuid; + $this->resourceUuid = $resource->uuid; + $this->resourceDbType = $resource->type(); + } + + if (str($resource->status)->startsWith('running')) { + $this->containers[] = $this->container; } if ( - $this->resource->getMorphClass() === \App\Models\StandaloneRedis::class || - $this->resource->getMorphClass() === \App\Models\StandaloneKeydb::class || - $this->resource->getMorphClass() === \App\Models\StandaloneDragonfly::class || - $this->resource->getMorphClass() === \App\Models\StandaloneClickhouse::class + $resource->getMorphClass() === \App\Models\StandaloneRedis::class || + $resource->getMorphClass() === \App\Models\StandaloneKeydb::class || + $resource->getMorphClass() === \App\Models\StandaloneDragonfly::class || + $resource->getMorphClass() === \App\Models\StandaloneClickhouse::class ) { $this->unsupported = true; } - + // Mark unsupported ServiceDatabase types (Redis, KeyDB, etc.) - if ($this->resource->getMorphClass() === \App\Models\ServiceDatabase::class) { - $dbType = $this->resource->databaseType(); - if (str_contains($dbType, 'redis') || str_contains($dbType, 'keydb') || + if ($resource->getMorphClass() === \App\Models\ServiceDatabase::class) { + $dbType = $resource->databaseType(); + if (str_contains($dbType, 'redis') || str_contains($dbType, 'keydb') || str_contains($dbType, 'dragonfly') || str_contains($dbType, 'clickhouse')) { $this->unsupported = true; } @@ -294,6 +379,12 @@ public function checkFile() return; } + if (! $this->server) { + $this->dispatch('error', 'Server not found. Please refresh the page.'); + + return; + } + try { $escapedPath = escapeshellarg($this->customLocation); $result = instant_remote_process(["ls -l {$escapedPath}"], $this->server, throwError: false); @@ -319,15 +410,22 @@ public function runImport() return; } + + if (! $this->server) { + $this->dispatch('error', 'Server not found. Please refresh the page.'); + + return; + } + try { $this->importRunning = true; $this->importCommands = []; - $backupFileName = "upload/{$this->resource->uuid}/restore"; + $backupFileName = "upload/{$this->resourceUuid}/restore"; // Check if an uploaded file exists first (takes priority over custom location) if (Storage::exists($backupFileName)) { $path = Storage::path($backupFileName); - $tmpPath = '/tmp/'.basename($backupFileName).'_'.$this->resource->uuid; + $tmpPath = '/tmp/'.basename($backupFileName).'_'.$this->resourceUuid; instant_scp($path, $tmpPath, $this->server); Storage::delete($backupFileName); $this->importCommands[] = "docker cp {$tmpPath} {$this->container}:{$tmpPath}"; @@ -338,7 +436,7 @@ public function runImport() return; } - $tmpPath = '/tmp/restore_'.$this->resource->uuid; + $tmpPath = '/tmp/restore_'.$this->resourceUuid; $escapedCustomLocation = escapeshellarg($this->customLocation); $this->importCommands[] = "docker cp {$escapedCustomLocation} {$this->container}:{$tmpPath}"; } else { @@ -348,7 +446,7 @@ public function runImport() } // Copy the restore command to a script file - $scriptPath = "/tmp/restore_{$this->resource->uuid}.sh"; + $scriptPath = "/tmp/restore_{$this->resourceUuid}.sh"; $restoreCommand = $this->buildRestoreCommand($tmpPath); @@ -388,9 +486,11 @@ public function loadAvailableS3Storages() try { $this->availableS3Storages = S3Storage::ownedByCurrentTeam(['id', 'name', 'description']) ->where('is_usable', true) - ->get(); + ->get() + ->map(fn ($s) => ['id' => $s->id, 'name' => $s->name, 'description' => $s->description]) + ->toArray(); } catch (\Throwable $e) { - $this->availableS3Storages = collect(); + $this->availableS3Storages = []; } } @@ -493,6 +593,12 @@ public function restoreFromS3() return; } + if (! $this->server) { + $this->dispatch('error', 'Server not found. Please refresh the page.'); + + return; + } + try { $this->importRunning = true; @@ -526,14 +632,18 @@ public function restoreFromS3() $fullImageName = "{$helperImage}:{$latestVersion}"; // Get the database destination network - $destinationNetwork = $this->resource->destination->network ?? 'coolify'; + if ($this->resource->getMorphClass() === \App\Models\ServiceDatabase::class) { + $destinationNetwork = $this->resource->service->destination->network ?? 'coolify'; + } else { + $destinationNetwork = $this->resource->destination->network ?? 'coolify'; + } // Generate unique names for this operation - $containerName = "s3-restore-{$this->resource->uuid}"; + $containerName = "s3-restore-{$this->resourceUuid}"; $helperTmpPath = '/tmp/'.basename($cleanPath); - $serverTmpPath = "/tmp/s3-restore-{$this->resource->uuid}-".basename($cleanPath); - $containerTmpPath = "/tmp/restore_{$this->resource->uuid}-".basename($cleanPath); - $scriptPath = "/tmp/restore_{$this->resource->uuid}.sh"; + $serverTmpPath = "/tmp/s3-restore-{$this->resourceUuid}-".basename($cleanPath); + $containerTmpPath = "/tmp/restore_{$this->resourceUuid}-".basename($cleanPath); + $scriptPath = "/tmp/restore_{$this->resourceUuid}.sh"; // Prepare all commands in sequence $commands = []; @@ -609,7 +719,7 @@ public function restoreFromS3() public function buildRestoreCommand(string $tmpPath): string { $morphClass = $this->resource->getMorphClass(); - + // Handle ServiceDatabase by checking the database type if ($morphClass === \App\Models\ServiceDatabase::class) { $dbType = $this->resource->databaseType(); @@ -623,7 +733,7 @@ public function buildRestoreCommand(string $tmpPath): string $morphClass = 'mongodb'; } } - + switch ($morphClass) { case \App\Models\StandaloneMariadb::class: case 'mariadb': diff --git a/app/Livewire/Project/Service/Database.php b/app/Livewire/Project/Service/Database.php deleted file mode 100644 index 301c39dee..000000000 --- a/app/Livewire/Project/Service/Database.php +++ /dev/null @@ -1,228 +0,0 @@ - 'nullable', - 'description' => 'nullable', - 'image' => 'required', - 'excludeFromStatus' => 'required|boolean', - 'publicPort' => 'nullable|integer', - 'isPublic' => 'required|boolean', - 'isLogDrainEnabled' => 'required|boolean', - ]; - - public function render() - { - return view('livewire.project.service.database'); - } - - public function mount() - { - try { - $this->parameters = get_route_parameters(); - $this->authorize('view', $this->database); - if ($this->database->is_public) { - $this->db_url_public = $this->database->getServiceDatabaseUrl(); - } - $this->refreshFileStorages(); - $this->syncData(false); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - private function syncData(bool $toModel = false): void - { - if ($toModel) { - $this->database->human_name = $this->humanName; - $this->database->description = $this->description; - $this->database->image = $this->image; - $this->database->exclude_from_status = $this->excludeFromStatus; - $this->database->public_port = $this->publicPort; - $this->database->is_public = $this->isPublic; - $this->database->is_log_drain_enabled = $this->isLogDrainEnabled; - } else { - $this->humanName = $this->database->human_name; - $this->description = $this->database->description; - $this->image = $this->database->image; - $this->excludeFromStatus = $this->database->exclude_from_status ?? false; - $this->publicPort = $this->database->public_port; - $this->isPublic = $this->database->is_public ?? false; - $this->isLogDrainEnabled = $this->database->is_log_drain_enabled ?? false; - } - } - - public function delete($password) - { - try { - $this->authorize('delete', $this->database); - - if (! verifyPasswordConfirmation($password, $this)) { - return; - } - - $this->database->delete(); - $this->dispatch('success', 'Database deleted.'); - - return redirectRoute($this, 'project.service.configuration', $this->parameters); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function instantSaveExclude() - { - try { - $this->authorize('update', $this->database); - $this->submit(); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function instantSaveLogDrain() - { - try { - $this->authorize('update', $this->database); - if (! $this->database->service->destination->server->isLogDrainEnabled()) { - $this->isLogDrainEnabled = false; - $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); - - return; - } - $this->submit(); - $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function convertToApplication() - { - try { - $this->authorize('update', $this->database); - $service = $this->database->service; - $serviceDatabase = $this->database; - - // Check if application with same name already exists - if ($service->applications()->where('name', $serviceDatabase->name)->exists()) { - throw new \Exception('An application with this name already exists.'); - } - - // Create new parameters removing database_uuid - $redirectParams = collect($this->parameters) - ->except('database_uuid') - ->all(); - - DB::transaction(function () use ($service, $serviceDatabase) { - $service->applications()->create([ - 'name' => $serviceDatabase->name, - 'human_name' => $serviceDatabase->human_name, - 'description' => $serviceDatabase->description, - 'exclude_from_status' => $serviceDatabase->exclude_from_status, - 'is_log_drain_enabled' => $serviceDatabase->is_log_drain_enabled, - 'image' => $serviceDatabase->image, - 'service_id' => $service->id, - 'is_migrated' => true, - ]); - $serviceDatabase->delete(); - }); - - return redirectRoute($this, 'project.service.configuration', $redirectParams); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function instantSave() - { - try { - $this->authorize('update', $this->database); - if ($this->isPublic && ! $this->publicPort) { - $this->dispatch('error', 'Public port is required.'); - $this->isPublic = false; - - return; - } - $this->syncData(true); - if ($this->database->is_public) { - if (! str($this->database->status)->startsWith('running')) { - $this->dispatch('error', 'Database must be started to be publicly accessible.'); - $this->isPublic = false; - $this->database->is_public = false; - - return; - } - StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->getServiceDatabaseUrl(); - $this->dispatch('success', 'Database is now publicly accessible.'); - } else { - StopDatabaseProxy::run($this->database); - $this->db_url_public = null; - $this->dispatch('success', 'Database is no longer publicly accessible.'); - } - $this->submit(); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function refreshFileStorages() - { - $this->fileStorages = $this->database->fileStorages()->get(); - } - - public function submit() - { - try { - $this->authorize('update', $this->database); - $this->validate(); - $this->syncData(true); - $this->database->save(); - $this->database->refresh(); - $this->syncData(false); - updateCompose($this->database); - $this->dispatch('success', 'Database saved.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } finally { - $this->dispatch('generateDockerCompose'); - } - } -} diff --git a/app/Livewire/Project/Service/DatabaseBackups.php b/app/Livewire/Project/Service/DatabaseBackups.php new file mode 100644 index 000000000..826a6c1ff --- /dev/null +++ b/app/Livewire/Project/Service/DatabaseBackups.php @@ -0,0 +1,64 @@ + '$refresh']; + + public function mount() + { + try { + $this->parameters = get_route_parameters(); + $this->query = request()->query(); + $this->service = Service::whereUuid($this->parameters['service_uuid'])->first(); + if (! $this->service) { + return redirect()->route('dashboard'); + } + $this->authorize('view', $this->service); + + $this->serviceDatabase = $this->service->databases()->whereUuid($this->parameters['stack_service_uuid'])->first(); + if (! $this->serviceDatabase) { + return redirect()->route('project.service.configuration', [ + 'project_uuid' => $this->parameters['project_uuid'], + 'environment_uuid' => $this->parameters['environment_uuid'], + 'service_uuid' => $this->parameters['service_uuid'], + ]); + } + + // Check if backups are supported for this database + if (! $this->serviceDatabase->isBackupSolutionAvailable() && ! $this->serviceDatabase->is_migrated) { + return redirect()->route('project.service.index', $this->parameters); + } + + // Check if import is supported for this database type + $dbType = $this->serviceDatabase->databaseType(); + $supportedTypes = ['mysql', 'mariadb', 'postgres', 'mongo']; + $this->isImportSupported = collect($supportedTypes)->contains(fn ($type) => str_contains($dbType, $type)); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.project.service.database-backups'); + } +} diff --git a/app/Livewire/Project/Service/Index.php b/app/Livewire/Project/Service/Index.php index 50772101a..aa678922d 100644 --- a/app/Livewire/Project/Service/Index.php +++ b/app/Livewire/Project/Service/Index.php @@ -2,12 +2,17 @@ namespace App\Livewire\Project\Service; +use App\Actions\Database\StartDatabaseProxy; +use App\Actions\Database\StopDatabaseProxy; +use App\Models\Server; use App\Models\Service; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\DB; use Livewire\Component; +use Spatie\Url\Url; class Index extends Component { @@ -19,6 +24,10 @@ class Index extends Component public ?ServiceDatabase $serviceDatabase = null; + public ?string $resourceType = null; + + public ?string $currentRoute = null; + public array $parameters; public array $query; @@ -27,7 +36,67 @@ class Index extends Component public $s3s; - protected $listeners = ['generateDockerCompose', 'refreshScheduledBackups' => '$refresh']; + public ?Server $server = null; + + // Database-specific properties + public ?string $db_url_public = null; + + public $fileStorages; + + public ?string $humanName = null; + + public ?string $description = null; + + public ?string $image = null; + + public bool $excludeFromStatus = false; + + public ?int $publicPort = null; + + public bool $isPublic = false; + + public bool $isLogDrainEnabled = false; + + public bool $isImportSupported = false; + + // Application-specific properties + public $docker_cleanup = true; + + public $delete_volumes = true; + + public $domainConflicts = []; + + public $showDomainConflictModal = false; + + public $forceSaveDomains = false; + + public $showPortWarningModal = false; + + public $forceRemovePort = false; + + public $requiredPort = null; + + public ?string $fqdn = null; + + public bool $isGzipEnabled = false; + + public bool $isStripprefixEnabled = false; + + protected $listeners = ['generateDockerCompose', 'refreshScheduledBackups' => '$refresh', 'refreshFileStorages']; + + protected $rules = [ + 'humanName' => 'nullable', + 'description' => 'nullable', + 'image' => 'required', + 'excludeFromStatus' => 'required|boolean', + 'publicPort' => 'nullable|integer', + 'isPublic' => 'required|boolean', + 'isLogDrainEnabled' => 'required|boolean', + // Application-specific rules + 'fqdn' => 'nullable', + 'isGzipEnabled' => 'nullable|boolean', + 'isStripprefixEnabled' => 'nullable|boolean', + ]; public function mount() { @@ -35,6 +104,7 @@ public function mount() $this->services = collect([]); $this->parameters = get_route_parameters(); $this->query = request()->query(); + $this->currentRoute = request()->route()->getName(); $this->service = Service::whereUuid($this->parameters['service_uuid'])->first(); if (! $this->service) { return redirect()->route('dashboard'); @@ -43,7 +113,9 @@ public function mount() $service = $this->service->applications()->whereUuid($this->parameters['stack_service_uuid'])->first(); if ($service) { $this->serviceApplication = $service; + $this->resourceType = 'application'; $this->serviceApplication->getFilesFromServer(); + $this->initializeApplicationProperties(); } else { $this->serviceDatabase = $this->service->databases()->whereUuid($this->parameters['stack_service_uuid'])->first(); if (! $this->serviceDatabase) { @@ -53,7 +125,9 @@ public function mount() 'service_uuid' => $this->parameters['service_uuid'], ]); } + $this->resourceType = 'database'; $this->serviceDatabase->getFilesFromServer(); + $this->initializeDatabaseProperties(); } $this->s3s = currentTeam()->s3s; } catch (\Throwable $e) { @@ -61,6 +135,42 @@ public function mount() } } + private function initializeDatabaseProperties(): void + { + $this->server = $this->serviceDatabase->service->destination->server; + if ($this->serviceDatabase->is_public) { + $this->db_url_public = $this->serviceDatabase->getServiceDatabaseUrl(); + } + $this->refreshFileStorages(); + $this->syncDatabaseData(false); + + // Check if import is supported for this database type + $dbType = $this->serviceDatabase->databaseType(); + $supportedTypes = ['mysql', 'mariadb', 'postgres', 'mongo']; + $this->isImportSupported = collect($supportedTypes)->contains(fn ($type) => str_contains($dbType, $type)); + } + + private function syncDatabaseData(bool $toModel = false): void + { + if ($toModel) { + $this->serviceDatabase->human_name = $this->humanName; + $this->serviceDatabase->description = $this->description; + $this->serviceDatabase->image = $this->image; + $this->serviceDatabase->exclude_from_status = $this->excludeFromStatus; + $this->serviceDatabase->public_port = $this->publicPort; + $this->serviceDatabase->is_public = $this->isPublic; + $this->serviceDatabase->is_log_drain_enabled = $this->isLogDrainEnabled; + } else { + $this->humanName = $this->serviceDatabase->human_name; + $this->description = $this->serviceDatabase->description; + $this->image = $this->serviceDatabase->image; + $this->excludeFromStatus = $this->serviceDatabase->exclude_from_status ?? false; + $this->publicPort = $this->serviceDatabase->public_port; + $this->isPublic = $this->serviceDatabase->is_public ?? false; + $this->isLogDrainEnabled = $this->serviceDatabase->is_log_drain_enabled ?? false; + } + } + public function generateDockerCompose() { try { @@ -71,6 +181,376 @@ public function generateDockerCompose() } } + // Database-specific methods + public function refreshFileStorages() + { + if ($this->serviceDatabase) { + $this->fileStorages = $this->serviceDatabase->fileStorages()->get(); + } + } + + public function deleteDatabase($password) + { + try { + $this->authorize('delete', $this->serviceDatabase); + + if (! verifyPasswordConfirmation($password, $this)) { + return; + } + + $this->serviceDatabase->delete(); + $this->dispatch('success', 'Database deleted.'); + + return redirectRoute($this, 'project.service.configuration', $this->parameters); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function instantSaveExclude() + { + try { + $this->authorize('update', $this->serviceDatabase); + $this->submitDatabase(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function instantSaveLogDrain() + { + try { + $this->authorize('update', $this->serviceDatabase); + if (! $this->serviceDatabase->service->destination->server->isLogDrainEnabled()) { + $this->isLogDrainEnabled = false; + $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); + + return; + } + $this->submitDatabase(); + $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function convertToApplication() + { + try { + $this->authorize('update', $this->serviceDatabase); + $service = $this->serviceDatabase->service; + $serviceDatabase = $this->serviceDatabase; + + // Check if application with same name already exists + if ($service->applications()->where('name', $serviceDatabase->name)->exists()) { + throw new \Exception('An application with this name already exists.'); + } + + // Create new parameters removing database_uuid + $redirectParams = collect($this->parameters) + ->except('database_uuid') + ->all(); + + DB::transaction(function () use ($service, $serviceDatabase) { + $service->applications()->create([ + 'name' => $serviceDatabase->name, + 'human_name' => $serviceDatabase->human_name, + 'description' => $serviceDatabase->description, + 'exclude_from_status' => $serviceDatabase->exclude_from_status, + 'is_log_drain_enabled' => $serviceDatabase->is_log_drain_enabled, + 'image' => $serviceDatabase->image, + 'service_id' => $service->id, + 'is_migrated' => true, + ]); + $serviceDatabase->delete(); + }); + + return redirectRoute($this, 'project.service.configuration', $redirectParams); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function instantSave() + { + try { + $this->authorize('update', $this->serviceDatabase); + if ($this->isPublic && ! $this->publicPort) { + $this->dispatch('error', 'Public port is required.'); + $this->isPublic = false; + + return; + } + $this->syncDatabaseData(true); + if ($this->serviceDatabase->is_public) { + if (! str($this->serviceDatabase->status)->startsWith('running')) { + $this->dispatch('error', 'Database must be started to be publicly accessible.'); + $this->isPublic = false; + $this->serviceDatabase->is_public = false; + + return; + } + StartDatabaseProxy::run($this->serviceDatabase); + $this->db_url_public = $this->serviceDatabase->getServiceDatabaseUrl(); + $this->dispatch('success', 'Database is now publicly accessible.'); + } else { + StopDatabaseProxy::run($this->serviceDatabase); + $this->db_url_public = null; + $this->dispatch('success', 'Database is no longer publicly accessible.'); + } + $this->submitDatabase(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function submitDatabase() + { + try { + $this->authorize('update', $this->serviceDatabase); + $this->validate(); + $this->syncDatabaseData(true); + $this->serviceDatabase->save(); + $this->serviceDatabase->refresh(); + $this->syncDatabaseData(false); + updateCompose($this->serviceDatabase); + $this->dispatch('success', 'Database saved.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } finally { + $this->dispatch('generateDockerCompose'); + } + } + + // Application-specific methods + private function initializeApplicationProperties(): void + { + $this->requiredPort = $this->serviceApplication->getRequiredPort(); + $this->syncApplicationData(false); + } + + private function syncApplicationData(bool $toModel = false): void + { + if ($toModel) { + $this->serviceApplication->human_name = $this->humanName; + $this->serviceApplication->description = $this->description; + $this->serviceApplication->fqdn = $this->fqdn; + $this->serviceApplication->image = $this->image; + $this->serviceApplication->exclude_from_status = $this->excludeFromStatus; + $this->serviceApplication->is_log_drain_enabled = $this->isLogDrainEnabled; + $this->serviceApplication->is_gzip_enabled = $this->isGzipEnabled; + $this->serviceApplication->is_stripprefix_enabled = $this->isStripprefixEnabled; + } else { + $this->humanName = $this->serviceApplication->human_name; + $this->description = $this->serviceApplication->description; + $this->fqdn = $this->serviceApplication->fqdn; + $this->image = $this->serviceApplication->image; + $this->excludeFromStatus = data_get($this->serviceApplication, 'exclude_from_status', false); + $this->isLogDrainEnabled = data_get($this->serviceApplication, 'is_log_drain_enabled', false); + $this->isGzipEnabled = data_get($this->serviceApplication, 'is_gzip_enabled', true); + $this->isStripprefixEnabled = data_get($this->serviceApplication, 'is_stripprefix_enabled', true); + } + } + + public function instantSaveApplication() + { + try { + $this->authorize('update', $this->serviceApplication); + $this->submitApplication(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function instantSaveApplicationSettings() + { + try { + $this->authorize('update', $this->serviceApplication); + $this->serviceApplication->is_gzip_enabled = $this->isGzipEnabled; + $this->serviceApplication->is_stripprefix_enabled = $this->isStripprefixEnabled; + $this->serviceApplication->exclude_from_status = $this->excludeFromStatus; + $this->serviceApplication->save(); + $this->dispatch('success', 'Settings saved.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function instantSaveApplicationAdvanced() + { + try { + $this->authorize('update', $this->serviceApplication); + if (! $this->serviceApplication->service->destination->server->isLogDrainEnabled()) { + $this->isLogDrainEnabled = false; + $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); + + return; + } + $this->syncApplicationData(true); + $this->serviceApplication->save(); + $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function deleteApplication($password) + { + try { + $this->authorize('delete', $this->serviceApplication); + + if (! verifyPasswordConfirmation($password, $this)) { + return; + } + + $this->serviceApplication->delete(); + $this->dispatch('success', 'Application deleted.'); + + return redirect()->route('project.service.configuration', $this->parameters); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function convertToDatabase() + { + try { + $this->authorize('update', $this->serviceApplication); + $service = $this->serviceApplication->service; + $serviceApplication = $this->serviceApplication; + + if ($service->databases()->where('name', $serviceApplication->name)->exists()) { + throw new \Exception('A database with this name already exists.'); + } + + $redirectParams = collect($this->parameters) + ->except('database_uuid') + ->all(); + DB::transaction(function () use ($service, $serviceApplication) { + $service->databases()->create([ + 'name' => $serviceApplication->name, + 'human_name' => $serviceApplication->human_name, + 'description' => $serviceApplication->description, + 'exclude_from_status' => $serviceApplication->exclude_from_status, + 'is_log_drain_enabled' => $serviceApplication->is_log_drain_enabled, + 'image' => $serviceApplication->image, + 'service_id' => $service->id, + 'is_migrated' => true, + ]); + $serviceApplication->delete(); + }); + + return redirect()->route('project.service.configuration', $redirectParams); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function confirmDomainUsage() + { + $this->forceSaveDomains = true; + $this->showDomainConflictModal = false; + $this->submitApplication(); + } + + public function confirmRemovePort() + { + $this->forceRemovePort = true; + $this->showPortWarningModal = false; + $this->submitApplication(); + } + + public function cancelRemovePort() + { + $this->showPortWarningModal = false; + $this->syncApplicationData(false); + } + + public function submitApplication() + { + try { + $this->authorize('update', $this->serviceApplication); + $this->fqdn = str($this->fqdn)->replaceEnd(',', '')->trim()->toString(); + $this->fqdn = str($this->fqdn)->replaceStart(',', '')->trim()->toString(); + $domains = str($this->fqdn)->trim()->explode(',')->map(function ($domain) { + $domain = trim($domain); + Url::fromString($domain, ['http', 'https']); + + return str($domain)->lower(); + }); + $this->fqdn = $domains->unique()->implode(','); + $warning = sslipDomainWarning($this->fqdn); + if ($warning) { + $this->dispatch('warning', __('warning.sslipdomain')); + } + + $this->syncApplicationData(true); + + if (! $this->forceSaveDomains) { + $result = checkDomainUsage(resource: $this->serviceApplication); + if ($result['hasConflicts']) { + $this->domainConflicts = $result['conflicts']; + $this->showDomainConflictModal = true; + + return; + } + } else { + $this->forceSaveDomains = false; + } + + if (! $this->forceRemovePort) { + $requiredPort = $this->serviceApplication->getRequiredPort(); + + if ($requiredPort !== null) { + $fqdns = str($this->fqdn)->trim()->explode(','); + $missingPort = false; + + foreach ($fqdns as $fqdn) { + $fqdn = trim($fqdn); + if (empty($fqdn)) { + continue; + } + + $port = ServiceApplication::extractPortFromUrl($fqdn); + if ($port === null) { + $missingPort = true; + break; + } + } + + if ($missingPort) { + $this->requiredPort = $requiredPort; + $this->showPortWarningModal = true; + + return; + } + } + } else { + $this->forceRemovePort = false; + } + + $this->validate(); + $this->serviceApplication->save(); + $this->serviceApplication->refresh(); + $this->syncApplicationData(false); + updateCompose($this->serviceApplication); + if (str($this->serviceApplication->fqdn)->contains(',')) { + $this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.

Only use multiple domains if you know what you are doing.'); + } else { + ! $warning && $this->dispatch('success', 'Service saved.'); + } + $this->dispatch('generateDockerCompose'); + } catch (\Throwable $e) { + $originalFqdn = $this->serviceApplication->getOriginal('fqdn'); + if ($originalFqdn !== $this->serviceApplication->fqdn) { + $this->serviceApplication->fqdn = $originalFqdn; + $this->syncApplicationData(false); + } + + return handleError($e, $this); + } + } + public function render() { return view('livewire.project.service.index'); diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php deleted file mode 100644 index 4302c05fb..000000000 --- a/app/Livewire/Project/Service/ServiceApplicationView.php +++ /dev/null @@ -1,345 +0,0 @@ - 'nullable', - 'description' => 'nullable', - 'fqdn' => 'nullable', - 'image' => 'string|nullable', - 'excludeFromStatus' => 'required|boolean', - 'application.required_fqdn' => 'required|boolean', - 'isLogDrainEnabled' => 'nullable|boolean', - 'isGzipEnabled' => 'nullable|boolean', - 'isStripprefixEnabled' => 'nullable|boolean', - ]; - - public function instantSave() - { - try { - $this->authorize('update', $this->application); - $this->submit(); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function instantSaveSettings() - { - try { - $this->authorize('update', $this->application); - // Save checkbox states without port validation - $this->application->is_gzip_enabled = $this->isGzipEnabled; - $this->application->is_stripprefix_enabled = $this->isStripprefixEnabled; - $this->application->exclude_from_status = $this->excludeFromStatus; - $this->application->save(); - $this->dispatch('success', 'Settings saved.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function instantSaveAdvanced() - { - try { - $this->authorize('update', $this->application); - if (! $this->application->service->destination->server->isLogDrainEnabled()) { - $this->isLogDrainEnabled = false; - $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); - - return; - } - // Sync component properties to model - $this->application->human_name = $this->humanName; - $this->application->description = $this->description; - $this->application->fqdn = $this->fqdn; - $this->application->image = $this->image; - $this->application->exclude_from_status = $this->excludeFromStatus; - $this->application->is_log_drain_enabled = $this->isLogDrainEnabled; - $this->application->is_gzip_enabled = $this->isGzipEnabled; - $this->application->is_stripprefix_enabled = $this->isStripprefixEnabled; - $this->application->save(); - $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function delete($password) - { - try { - $this->authorize('delete', $this->application); - - if (! verifyPasswordConfirmation($password, $this)) { - return; - } - - $this->application->delete(); - $this->dispatch('success', 'Application deleted.'); - - return redirect()->route('project.service.configuration', $this->parameters); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function mount() - { - try { - $this->parameters = get_route_parameters(); - $this->authorize('view', $this->application); - $this->requiredPort = $this->application->getRequiredPort(); - $this->syncData(); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function confirmRemovePort() - { - $this->forceRemovePort = true; - $this->showPortWarningModal = false; - $this->submit(); - } - - public function cancelRemovePort() - { - $this->showPortWarningModal = false; - $this->syncData(); // Reset to original FQDN - } - - public function syncData(bool $toModel = false): void - { - if ($toModel) { - $this->validate(); - - // Sync to model - $this->application->human_name = $this->humanName; - $this->application->description = $this->description; - $this->application->fqdn = $this->fqdn; - $this->application->image = $this->image; - $this->application->exclude_from_status = $this->excludeFromStatus; - $this->application->is_log_drain_enabled = $this->isLogDrainEnabled; - $this->application->is_gzip_enabled = $this->isGzipEnabled; - $this->application->is_stripprefix_enabled = $this->isStripprefixEnabled; - - $this->application->save(); - } else { - // Sync from model - $this->humanName = $this->application->human_name; - $this->description = $this->application->description; - $this->fqdn = $this->application->fqdn; - $this->image = $this->application->image; - $this->excludeFromStatus = data_get($this->application, 'exclude_from_status', false); - $this->isLogDrainEnabled = data_get($this->application, 'is_log_drain_enabled', false); - $this->isGzipEnabled = data_get($this->application, 'is_gzip_enabled', true); - $this->isStripprefixEnabled = data_get($this->application, 'is_stripprefix_enabled', true); - } - } - - public function convertToDatabase() - { - try { - $this->authorize('update', $this->application); - $service = $this->application->service; - $serviceApplication = $this->application; - - // Check if database with same name already exists - if ($service->databases()->where('name', $serviceApplication->name)->exists()) { - throw new \Exception('A database with this name already exists.'); - } - - $redirectParams = collect($this->parameters) - ->except('database_uuid') - ->all(); - DB::transaction(function () use ($service, $serviceApplication) { - $service->databases()->create([ - 'name' => $serviceApplication->name, - 'human_name' => $serviceApplication->human_name, - 'description' => $serviceApplication->description, - 'exclude_from_status' => $serviceApplication->exclude_from_status, - 'is_log_drain_enabled' => $serviceApplication->is_log_drain_enabled, - 'image' => $serviceApplication->image, - 'service_id' => $service->id, - 'is_migrated' => true, - ]); - $serviceApplication->delete(); - }); - - return redirect()->route('project.service.configuration', $redirectParams); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function confirmDomainUsage() - { - $this->forceSaveDomains = true; - $this->showDomainConflictModal = false; - $this->submit(); - } - - public function submit() - { - try { - $this->authorize('update', $this->application); - $this->fqdn = str($this->fqdn)->replaceEnd(',', '')->trim()->toString(); - $this->fqdn = str($this->fqdn)->replaceStart(',', '')->trim()->toString(); - $domains = str($this->fqdn)->trim()->explode(',')->map(function ($domain) { - $domain = trim($domain); - Url::fromString($domain, ['http', 'https']); - - return str($domain)->lower(); - }); - $this->fqdn = $domains->unique()->implode(','); - $warning = sslipDomainWarning($this->fqdn); - if ($warning) { - $this->dispatch('warning', __('warning.sslipdomain')); - } - // Sync to model for domain conflict check (without validation) - $this->application->human_name = $this->humanName; - $this->application->description = $this->description; - $this->application->fqdn = $this->fqdn; - $this->application->image = $this->image; - $this->application->exclude_from_status = $this->excludeFromStatus; - $this->application->is_log_drain_enabled = $this->isLogDrainEnabled; - $this->application->is_gzip_enabled = $this->isGzipEnabled; - $this->application->is_stripprefix_enabled = $this->isStripprefixEnabled; - // Check for domain conflicts if not forcing save - if (! $this->forceSaveDomains) { - $result = checkDomainUsage(resource: $this->application); - if ($result['hasConflicts']) { - $this->domainConflicts = $result['conflicts']; - $this->showDomainConflictModal = true; - - return; - } - } else { - // Reset the force flag after using it - $this->forceSaveDomains = false; - } - - // Check for required port - if (! $this->forceRemovePort) { - $requiredPort = $this->application->getRequiredPort(); - - if ($requiredPort !== null) { - // Check if all FQDNs have a port - $fqdns = str($this->fqdn)->trim()->explode(','); - $missingPort = false; - - foreach ($fqdns as $fqdn) { - $fqdn = trim($fqdn); - if (empty($fqdn)) { - continue; - } - - $port = ServiceApplication::extractPortFromUrl($fqdn); - if ($port === null) { - $missingPort = true; - break; - } - } - - if ($missingPort) { - $this->requiredPort = $requiredPort; - $this->showPortWarningModal = true; - - return; - } - } - } else { - // Reset the force flag after using it - $this->forceRemovePort = false; - } - - $this->validate(); - $this->application->save(); - $this->application->refresh(); - $this->syncData(); - updateCompose($this->application); - if (str($this->application->fqdn)->contains(',')) { - $this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.

Only use multiple domains if you know what you are doing.'); - } else { - ! $warning && $this->dispatch('success', 'Service saved.'); - } - $this->dispatch('generateDockerCompose'); - } catch (\Throwable $e) { - $originalFqdn = $this->application->getOriginal('fqdn'); - if ($originalFqdn !== $this->application->fqdn) { - $this->application->fqdn = $originalFqdn; - $this->syncData(); - } - - return handleError($e, $this); - } - } - - 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.'] - ], - ]); - } -} diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 4db777732..f61790f52 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -551,7 +551,21 @@ function getResourceByUuid(string $uuid, ?int $teamId = null) return null; } $resource = queryResourcesByUuid($uuid); - if (! is_null($resource) && $resource->environment->project->team_id === $teamId) { + if (is_null($resource)) { + return null; + } + + // ServiceDatabase has a different relationship path: service->environment->project->team_id + if ($resource instanceof \App\Models\ServiceDatabase) { + if ($resource->service?->environment?->project?->team_id === $teamId) { + return $resource; + } + + return null; + } + + // Standard resources: environment->project->team_id + if ($resource->environment->project->team_id === $teamId) { return $resource; } @@ -638,6 +652,12 @@ function queryResourcesByUuid(string $uuid) return $clickhouse; } + // Check for ServiceDatabase by its own UUID + $serviceDatabase = ServiceDatabase::whereUuid($uuid)->first(); + if ($serviceDatabase) { + return $serviceDatabase; + } + return $resource; } function generateTagDeployWebhook($tag_name) diff --git a/resources/views/components/service-database/sidebar.blade.php b/resources/views/components/service-database/sidebar.blade.php new file mode 100644 index 000000000..728df3a7b --- /dev/null +++ b/resources/views/components/service-database/sidebar.blade.php @@ -0,0 +1,24 @@ +@props([ + 'parameters', + 'serviceDatabase', + 'isImportSupported' => false, +]) + +
+ + + + General + @if ($serviceDatabase?->isBackupSolutionAvailable() || $serviceDatabase?->is_migrated) + Backups + @endif + @if ($isImportSupported) + Import Backup + @endif +
diff --git a/resources/views/livewire/project/database/backup-executions.blade.php b/resources/views/livewire/project/database/backup-executions.blade.php index c56908f50..b6d88a2fd 100644 --- a/resources/views/livewire/project/database/backup-executions.blade.php +++ b/resources/views/livewire/project/database/backup-executions.blade.php @@ -186,10 +186,13 @@ class="flex flex-col gap-4">
No executions found.
@endforelse - @endisset + +@script + +@endscript diff --git a/resources/views/livewire/project/database/configuration.blade.php b/resources/views/livewire/project/database/configuration.blade.php index 928f27927..7460274e4 100644 --- a/resources/views/livewire/project/database/configuration.blade.php +++ b/resources/views/livewire/project/database/configuration.blade.php @@ -18,9 +18,9 @@ href="{{ route('project.database.persistent-storage', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Persistent Storage @can('update', $database) - Import - Backups + Import + Backup @endcan Webhooks @@ -63,7 +63,7 @@ @elseif ($currentRoute === 'project.database.persistent-storage') - @elseif ($currentRoute === 'project.database.import-backups') + @elseif ($currentRoute === 'project.database.import-backup') @elseif ($currentRoute === 'project.database.webhooks') diff --git a/resources/views/livewire/project/database/import.blade.php b/resources/views/livewire/project/database/import.blade.php index 3e8069d7e..666abb3b3 100644 --- a/resources/views/livewire/project/database/import.blade.php +++ b/resources/views/livewire/project/database/import.blade.php @@ -58,9 +58,9 @@ This is a destructive action, existing data will be replaced! - @if (str(data_get($resource, 'status'))->startsWith('running')) + @if (str($resourceStatus)->startsWith('running')) {{-- Restore Command Configuration --}} - @if ($resource->type() === 'standalone-postgresql') + @if ($resourceDbType === 'standalone-postgresql') @if ($dumpAll) @@ -75,7 +75,7 @@
- @elseif ($resource->type() === 'standalone-mysql') + @elseif ($resourceDbType === 'standalone-mysql') @if ($dumpAll) @@ -85,7 +85,7 @@
- @elseif ($resource->type() === 'standalone-mariadb') + @elseif ($resourceDbType === 'standalone-mariadb') @if ($dumpAll) @@ -112,7 +112,7 @@ class="flex-1 p-6 border-2 rounded-sm cursor-pointer transition-all" - @if ($availableS3Storages->count() > 0) + @if (count($availableS3Storages) > 0)
@@ -128,7 +128,7 @@ class="flex-1 p-6 border-2 rounded-sm cursor-pointer transition-all"
{{-- File Restore Section --}} - @can('update', $resource) + @can('update', $this->resource)

Backup File

@@ -139,7 +139,7 @@ class="flex-1 p-6 border-2 rounded-sm cursor-pointer transition-all"
Or
- + @csrf
@@ -168,17 +168,17 @@ class="flex-1 p-6 border-2 rounded-sm cursor-pointer transition-all" @endcan {{-- S3 Restore Section --}} - @if ($availableS3Storages->count() > 0) - @can('update', $resource) + @if (count($availableS3Storages) > 0) + @can('update', $this->resource)

Restore from S3

@foreach ($availableS3Storages as $storage) - @endforeach @@ -226,7 +226,7 @@ class="flex-1 p-6 border-2 rounded-sm cursor-pointer transition-all" Database Restore Output
- +
diff --git a/resources/views/livewire/project/service/configuration.blade.php b/resources/views/livewire/project/service/configuration.blade.php index ec8377deb..35eadeb33 100644 --- a/resources/views/livewire/project/service/configuration.blade.php +++ b/resources/views/livewire/project/service/configuration.blade.php @@ -155,7 +155,7 @@ class="w-4 h-4 dark:text-warning text-coollabs"
@if ($database->isBackupSolutionAvailable() || $database->is_migrated) + href="{{ route('project.service.database.backups', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid, 'stack_service_uuid' => $database->uuid]) }}"> Backups @endif diff --git a/resources/views/livewire/project/service/database-backups.blade.php b/resources/views/livewire/project/service/database-backups.blade.php new file mode 100644 index 000000000..f98d8c8e1 --- /dev/null +++ b/resources/views/livewire/project/service/database-backups.blade.php @@ -0,0 +1,23 @@ +
+ +
+ +
+ + {{ data_get_str($service, 'name')->limit(10) }} > + {{ data_get_str($serviceDatabase, 'name')->limit(10) }} > Backups | Coolify + +
+

Scheduled Backups

+ @if (filled($serviceDatabase->custom_type) || !$serviceDatabase->is_migrated) + @can('update', $serviceDatabase) + + + + @endcan + @endif +
+ +
+
+
diff --git a/resources/views/livewire/project/service/database.blade.php b/resources/views/livewire/project/service/database.blade.php deleted file mode 100644 index eebe78c3f..000000000 --- a/resources/views/livewire/project/service/database.blade.php +++ /dev/null @@ -1,67 +0,0 @@ -
-
-
- @if ($database->human_name) -

{{ Str::headline($database->human_name) }}

- @else -

{{ Str::headline($database->name) }}

- @endif - Save - @can('update', $database) - - @endcan - @can('delete', $database) - - @endcan -
-
-
- - - -
-
- - -
- @if ($db_url_public) - - @endif -
-

Advanced

-
- - -
-
- - @php - $dbType = $database->databaseType(); - $supportedTypes = ['mysql', 'mariadb', 'postgres', 'mongo']; - $isSupported = collect($supportedTypes)->contains(fn($type) => str_contains($dbType, $type)); - @endphp - - @if ($isSupported) - @can('update', $database) -
-

Import Backup

- -
- @endcan - @endif -
diff --git a/resources/views/livewire/project/service/index.blade.php b/resources/views/livewire/project/service/index.blade.php index b102292b9..ad995676c 100644 --- a/resources/views/livewire/project/service/index.blade.php +++ b/resources/views/livewire/project/service/index.blade.php @@ -1,54 +1,257 @@ -
+
-
- - - - General - @if ($serviceDatabase?->isBackupSolutionAvailable() || $serviceDatabase?->is_migrated) - Backups - @endif -
+ @if ($resourceType === 'database') + + @else + + @endif
- @isset($serviceApplication) + @if ($resourceType === 'application') {{ data_get_str($service, 'name')->limit(10) }} > {{ data_get_str($serviceApplication, 'name')->limit(10) }} | Coolify -
- -
- @endisset - @isset($serviceDatabase) +
+
+ @if ($serviceApplication->human_name) +

{{ Str::headline($serviceApplication->human_name) }}

+ @else +

{{ Str::headline($serviceApplication->name) }}

+ @endif + Save + @can('update', $serviceApplication) + + @endcan + @can('delete', $serviceApplication) + + @endcan +
+
+ @if ($requiredPort && !$serviceApplication->serviceType()?->contains(str($serviceApplication->image)->before(':'))) + + This service requires port {{ $requiredPort }} to function correctly. All domains must include this port number (or any other port if you know what you're doing). +

+ Example: http://app.coolify.io:{{ $requiredPort }} +
+ @endif + +
+ + +
+
+ @if (!$serviceApplication->serviceType()?->contains(str($serviceApplication->image)->before(':'))) + @if ($serviceApplication->required_fqdn) + + @else + + @endif + @endif + +
+
+

Advanced

+
+ @if (str($serviceApplication->image)->contains('pocketbase')) + + @else + + @endif + + + +
+
+ + + +
    +
  • Only one service will be accessible at this domain
  • +
  • The routing behavior will be unpredictable
  • +
  • You may experience service disruptions
  • +
  • SSL certificates might not work correctly
  • +
+
+
+ + @if ($showPortWarningModal) +
+ +
+ @endif + @elseif ($resourceType === 'database') {{ data_get_str($service, 'name')->limit(10) }} > {{ data_get_str($serviceDatabase, 'name')->limit(10) }} | Coolify -
- -
- @if ($serviceDatabase?->isBackupSolutionAvailable() || $serviceDatabase?->is_migrated) -
-
-

Scheduled Backups

- @if (filled($serviceDatabase->custom_type) || !$serviceDatabase->is_migrated) - @can('update', $serviceDatabase) - - - - @endcan + @if ($currentRoute === 'project.service.database.import') + + @else +
+
+ @if ($serviceDatabase->human_name) +

{{ Str::headline($serviceDatabase->human_name) }}

+ @else +

{{ Str::headline($serviceDatabase->name) }}

@endif + Save + @can('update', $serviceDatabase) + + @endcan + @can('delete', $serviceDatabase) + + @endcan
- -
+
+
+ + + +
+
+
+

Proxy

+ + @if ($serviceDatabase->is_public) + + Proxy Logs + + + + Logs + + @endif +
+
+ +
+ + @if ($db_url_public) + + @endif +
+
+

Advanced

+
+ + +
+ @endif - @endisset + @endif
diff --git a/resources/views/livewire/project/service/service-application-view.blade.php b/resources/views/livewire/project/service/service-application-view.blade.php deleted file mode 100644 index f04e33817..000000000 --- a/resources/views/livewire/project/service/service-application-view.blade.php +++ /dev/null @@ -1,149 +0,0 @@ -
-
-
- @if ($application->human_name) -

{{ Str::headline($application->human_name) }}

- @else -

{{ Str::headline($application->name) }}

- @endif - Save - @can('update', $application) - - @endcan - @can('delete', $application) - - @endcan -
-
- @if($requiredPort && !$application->serviceType()?->contains(str($application->image)->before(':'))) - - This service requires port {{ $requiredPort }} to function correctly. All domains must include this port number (or any other port if you know what you're doing). -

- Example: http://app.coolify.io:{{ $requiredPort }} -
- @endif - -
- - -
-
- @if (!$application->serviceType()?->contains(str($application->image)->before(':'))) - @if ($application->required_fqdn) - - @else - - @endif - @endif - -
-
-

Advanced

-
- @if (str($application->image)->contains('pocketbase')) - - @else - - @endif - - - -
-
- - - -
    -
  • Only one service will be accessible at this domain
  • -
  • The routing behavior will be unpredictable
  • -
  • You may experience service disruptions
  • -
  • SSL certificates might not work correctly
  • -
-
-
- - @if ($showPortWarningModal) -
- -
- @endif -
diff --git a/routes/web.php b/routes/web.php index 2a9072299..e8c738b71 100644 --- a/routes/web.php +++ b/routes/web.php @@ -29,6 +29,7 @@ use App\Livewire\Project\Resource\Create as ResourceCreate; use App\Livewire\Project\Resource\Index as ResourceIndex; use App\Livewire\Project\Service\Configuration as ServiceConfiguration; +use App\Livewire\Project\Service\DatabaseBackups as ServiceDatabaseBackups; use App\Livewire\Project\Service\Index as ServiceIndex; use App\Livewire\Project\Shared\ExecuteContainerCommand; use App\Livewire\Project\Shared\Logs; @@ -218,7 +219,7 @@ Route::get('/', DatabaseConfiguration::class)->name('project.database.configuration'); Route::get('/environment-variables', DatabaseConfiguration::class)->name('project.database.environment-variables'); Route::get('/servers', DatabaseConfiguration::class)->name('project.database.servers'); - Route::get('/import-backups', DatabaseConfiguration::class)->name('project.database.import-backups')->middleware('can.update.resource'); + Route::get('/import-backup', DatabaseConfiguration::class)->name('project.database.import-backup')->middleware('can.update.resource'); Route::get('/persistent-storage', DatabaseConfiguration::class)->name('project.database.persistent-storage'); Route::get('/webhooks', DatabaseConfiguration::class)->name('project.database.webhooks'); Route::get('/resource-limits', DatabaseConfiguration::class)->name('project.database.resource-limits'); @@ -243,6 +244,8 @@ Route::get('/tags', ServiceConfiguration::class)->name('project.service.tags'); Route::get('/danger', ServiceConfiguration::class)->name('project.service.danger'); Route::get('/terminal', ExecuteContainerCommand::class)->name('project.service.command')->middleware('can.access.terminal'); + Route::get('/{stack_service_uuid}/backups', ServiceDatabaseBackups::class)->name('project.service.database.backups'); + Route::get('/{stack_service_uuid}/import', ServiceIndex::class)->name('project.service.database.import')->middleware('can.update.resource'); Route::get('/{stack_service_uuid}', ServiceIndex::class)->name('project.service.index'); Route::get('/tasks/{task_uuid}', ScheduledTaskShow::class)->name('project.service.scheduled-tasks'); });