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,
+])
+
+