feat: Refactor service database management and backup functionalities
- Introduced a new sidebar component for service database navigation. - Updated routes for database import and backup functionalities. - Refactored the database import view to improve clarity and maintainability. - Consolidated service application and database views into a more cohesive structure. - Removed deprecated service application view and integrated its functionalities into the service index. - Enhanced user experience with modal confirmations for critical actions. - Improved code readability and organization across various components.
This commit is contained in:
parent
9466ad4a48
commit
796bb3a19d
18 changed files with 1056 additions and 911 deletions
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -1,228 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Component;
|
||||
|
||||
class Database extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public ServiceDatabase $database;
|
||||
|
||||
public ?string $db_url_public = null;
|
||||
|
||||
public $fileStorages;
|
||||
|
||||
public $parameters;
|
||||
|
||||
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;
|
||||
|
||||
protected $listeners = ['refreshFileStorages'];
|
||||
|
||||
protected $rules = [
|
||||
'humanName' => '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');
|
||||
}
|
||||
}
|
||||
}
|
||||
64
app/Livewire/Project/Service/DatabaseBackups.php
Normal file
64
app/Livewire/Project/Service/DatabaseBackups.php
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
class DatabaseBackups extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public ?Service $service = null;
|
||||
|
||||
public ?ServiceDatabase $serviceDatabase = null;
|
||||
|
||||
public array $parameters;
|
||||
|
||||
public array $query;
|
||||
|
||||
public bool $isImportSupported = false;
|
||||
|
||||
protected $listeners = ['refreshScheduledBackups' => '$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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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.<br><br>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');
|
||||
|
|
|
|||
|
|
@ -1,345 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Models\ServiceApplication;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
|
||||
class ServiceApplicationView extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public ServiceApplication $application;
|
||||
|
||||
public $parameters;
|
||||
|
||||
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;
|
||||
|
||||
#[Validate(['nullable'])]
|
||||
public ?string $humanName = null;
|
||||
|
||||
#[Validate(['nullable'])]
|
||||
public ?string $description = null;
|
||||
|
||||
#[Validate(['nullable'])]
|
||||
public ?string $fqdn = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $image = null;
|
||||
|
||||
#[Validate(['required', 'boolean'])]
|
||||
public bool $excludeFromStatus = false;
|
||||
|
||||
#[Validate(['nullable', 'boolean'])]
|
||||
public bool $isLogDrainEnabled = false;
|
||||
|
||||
#[Validate(['nullable', 'boolean'])]
|
||||
public bool $isGzipEnabled = false;
|
||||
|
||||
#[Validate(['nullable', 'boolean'])]
|
||||
public bool $isStripprefixEnabled = false;
|
||||
|
||||
protected $rules = [
|
||||
'humanName' => '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.<br><br>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.']
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
@props([
|
||||
'parameters',
|
||||
'serviceDatabase',
|
||||
'isImportSupported' => false,
|
||||
])
|
||||
|
||||
<div class="flex flex-col items-start gap-2 min-w-fit">
|
||||
<a class="menu-item"
|
||||
class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-active' : '' }}"
|
||||
{{ wireNavigate() }}
|
||||
href="{{ route('project.service.configuration', [...$parameters, 'stack_service_uuid' => null]) }}">
|
||||
<button><- Back</button>
|
||||
</a>
|
||||
<a class="menu-item" wire:current.exact="menu-item-active" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.index', $parameters) }}">General</a>
|
||||
@if ($serviceDatabase?->isBackupSolutionAvailable() || $serviceDatabase?->is_migrated)
|
||||
<a class="menu-item" wire:current.exact="menu-item-active" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.database.backups', $parameters) }}">Backups</a>
|
||||
@endif
|
||||
@if ($isImportSupported)
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.service.database.import', $parameters) }}">Import Backup</a>
|
||||
@endif
|
||||
</div>
|
||||
|
|
@ -186,10 +186,13 @@ class="flex flex-col gap-4">
|
|||
<div class="p-4 bg-gray-100 dark:bg-coolgray-100 rounded-sm">No executions found.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
<script>
|
||||
function download_file(executionId) {
|
||||
window.open('/download/backup/' + executionId, '_blank');
|
||||
}
|
||||
</script>
|
||||
@endisset
|
||||
</div>
|
||||
|
||||
@script
|
||||
<script>
|
||||
window.download_file = function(executionId) {
|
||||
window.open('/download/backup/' + executionId, '_blank');
|
||||
}
|
||||
</script>
|
||||
@endscript
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@
|
|||
href="{{ route('project.database.persistent-storage', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Persistent
|
||||
Storage</a>
|
||||
@can('update', $database)
|
||||
<a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.import-backups', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Import
|
||||
Backups</a>
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.import-backup', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Import
|
||||
Backup</a>
|
||||
@endcan
|
||||
<a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.webhooks', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Webhooks</a>
|
||||
|
|
@ -63,7 +63,7 @@
|
|||
<livewire:project.shared.destination :resource="$database" />
|
||||
@elseif ($currentRoute === 'project.database.persistent-storage')
|
||||
<livewire:project.service.storage :resource="$database" />
|
||||
@elseif ($currentRoute === 'project.database.import-backups')
|
||||
@elseif ($currentRoute === 'project.database.import-backup')
|
||||
<livewire:project.database.import :resource="$database" />
|
||||
@elseif ($currentRoute === 'project.database.webhooks')
|
||||
<livewire:project.shared.webhooks :resource="$database" />
|
||||
|
|
|
|||
|
|
@ -58,9 +58,9 @@
|
|||
</svg>
|
||||
<span>This is a destructive action, existing data will be replaced!</span>
|
||||
</div>
|
||||
@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)
|
||||
<x-forms.textarea rows="6" readonly label="Custom Import Command"
|
||||
wire:model='restoreCommandText'></x-forms.textarea>
|
||||
|
|
@ -75,7 +75,7 @@
|
|||
<div class="w-64 pt-2">
|
||||
<x-forms.checkbox label="Backup includes all databases" wire:model.live='dumpAll'></x-forms.checkbox>
|
||||
</div>
|
||||
@elseif ($resource->type() === 'standalone-mysql')
|
||||
@elseif ($resourceDbType === 'standalone-mysql')
|
||||
@if ($dumpAll)
|
||||
<x-forms.textarea rows="14" readonly label="Custom Import Command"
|
||||
wire:model='restoreCommandText'></x-forms.textarea>
|
||||
|
|
@ -85,7 +85,7 @@
|
|||
<div class="w-64 pt-2">
|
||||
<x-forms.checkbox label="Backup includes all databases" wire:model.live='dumpAll'></x-forms.checkbox>
|
||||
</div>
|
||||
@elseif ($resource->type() === 'standalone-mariadb')
|
||||
@elseif ($resourceDbType === 'standalone-mariadb')
|
||||
@if ($dumpAll)
|
||||
<x-forms.textarea rows="14" readonly label="Custom Import Command"
|
||||
wire:model='restoreCommandText'></x-forms.textarea>
|
||||
|
|
@ -112,7 +112,7 @@ class="flex-1 p-6 border-2 rounded-sm cursor-pointer transition-all"
|
|||
</div>
|
||||
</div>
|
||||
|
||||
@if ($availableS3Storages->count() > 0)
|
||||
@if (count($availableS3Storages) > 0)
|
||||
<div @click="restoreType = 's3'"
|
||||
class="flex-1 p-6 border-2 rounded-sm cursor-pointer transition-all"
|
||||
:class="restoreType === 's3' ? 'border-warning bg-warning/10' : 'border-neutral-200 dark:border-neutral-800 hover:border-warning/50'">
|
||||
|
|
@ -128,7 +128,7 @@ class="flex-1 p-6 border-2 rounded-sm cursor-pointer transition-all"
|
|||
</div>
|
||||
|
||||
{{-- File Restore Section --}}
|
||||
@can('update', $resource)
|
||||
@can('update', $this->resource)
|
||||
<div x-show="restoreType === 'file'" class="pt-6">
|
||||
<h3>Backup File</h3>
|
||||
<form class="flex gap-2 items-end pt-2">
|
||||
|
|
@ -139,7 +139,7 @@ class="flex-1 p-6 border-2 rounded-sm cursor-pointer transition-all"
|
|||
<div class="pt-2 text-center text-xl font-bold">
|
||||
Or
|
||||
</div>
|
||||
<form action="/upload/backup/{{ $resource->uuid }}" class="dropzone" id="my-dropzone" wire:ignore>
|
||||
<form action="/upload/backup/{{ $resourceUuid }}" class="dropzone" id="my-dropzone" wire:ignore>
|
||||
@csrf
|
||||
</form>
|
||||
<div x-show="isUploading">
|
||||
|
|
@ -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)
|
||||
<div x-show="restoreType === 's3'" class="pt-6">
|
||||
<h3>Restore from S3</h3>
|
||||
<div class="flex flex-col gap-2 pt-2">
|
||||
<x-forms.select label="S3 Storage" wire:model.live="s3StorageId">
|
||||
<option value="">Select S3 Storage</option>
|
||||
@foreach ($availableS3Storages as $storage)
|
||||
<option value="{{ $storage->id }}">{{ $storage->name }}
|
||||
@if ($storage->description)
|
||||
- {{ $storage->description }}
|
||||
<option value="{{ $storage['id'] }}">{{ $storage['name'] }}
|
||||
@if ($storage['description'])
|
||||
- {{ $storage['description'] }}
|
||||
@endif
|
||||
</option>
|
||||
@endforeach
|
||||
|
|
@ -226,7 +226,7 @@ class="flex-1 p-6 border-2 rounded-sm cursor-pointer transition-all"
|
|||
<x-slot:title>Database Restore Output</x-slot:title>
|
||||
<x-slot:content>
|
||||
<div wire:ignore>
|
||||
<livewire:activity-monitor wire:key="database-restore-{{ $resource->uuid }}" header="Logs" fullHeight />
|
||||
<livewire:activity-monitor wire:key="database-restore-{{ $resourceUuid }}" header="Logs" fullHeight />
|
||||
</div>
|
||||
</x-slot:content>
|
||||
</x-slide-over>
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ class="w-4 h-4 dark:text-warning text-coollabs"
|
|||
<div class="flex items-center px-4">
|
||||
@if ($database->isBackupSolutionAvailable() || $database->is_migrated)
|
||||
<a class="mx-4 text-xs font-bold hover:underline" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.index', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid, 'stack_service_uuid' => $database->uuid]) }}#backups">
|
||||
href="{{ route('project.service.database.backups', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid, 'stack_service_uuid' => $database->uuid]) }}">
|
||||
Backups
|
||||
</a>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
<div>
|
||||
<livewire:project.service.heading :service="$service" :parameters="$parameters" :query="$query" />
|
||||
<div class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<x-service-database.sidebar :parameters="$parameters" :serviceDatabase="$serviceDatabase" :isImportSupported="$isImportSupported" />
|
||||
<div class="w-full">
|
||||
<x-slot:title>
|
||||
{{ data_get_str($service, 'name')->limit(10) }} >
|
||||
{{ data_get_str($serviceDatabase, 'name')->limit(10) }} > Backups | Coolify
|
||||
</x-slot>
|
||||
<div class="flex gap-2">
|
||||
<h2 class="pb-4">Scheduled Backups</h2>
|
||||
@if (filled($serviceDatabase->custom_type) || !$serviceDatabase->is_migrated)
|
||||
@can('update', $serviceDatabase)
|
||||
<x-modal-input buttonTitle="+ Add" title="New Scheduled Backup">
|
||||
<livewire:project.database.create-scheduled-backup :database="$serviceDatabase" />
|
||||
</x-modal-input>
|
||||
@endcan
|
||||
@endif
|
||||
</div>
|
||||
<livewire:project.database.scheduled-backups :database="$serviceDatabase" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
<div>
|
||||
<form wire:submit='submit'>
|
||||
<div class="flex items-center gap-2 pb-4">
|
||||
@if ($database->human_name)
|
||||
<h2>{{ Str::headline($database->human_name) }}</h2>
|
||||
@else
|
||||
<h2>{{ Str::headline($database->name) }}</h2>
|
||||
@endif
|
||||
<x-forms.button canGate="update" :canResource="$database" type="submit">Save</x-forms.button>
|
||||
@can('update', $database)
|
||||
<x-modal-confirmation wire:click="convertToApplication" title="Convert to Application"
|
||||
buttonTitle="Convert to Application" submitAction="convertToApplication" :actions="['The selected resource will be converted to an application.']"
|
||||
confirmationText="{{ Str::headline($database->name) }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Service Database Name below"
|
||||
shortConfirmationLabel="Service Database Name" />
|
||||
@endcan
|
||||
@can('delete', $database)
|
||||
<x-modal-confirmation title="Confirm Service Database Deletion?" buttonTitle="Delete" isErrorButton
|
||||
submitAction="delete" :actions="['The selected service database container will be stopped and permanently deleted.']" confirmationText="{{ Str::headline($database->name) }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Service Database Name below"
|
||||
shortConfirmationLabel="Service Database Name" />
|
||||
@endcan
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$database" label="Name" id="humanName" placeholder="Name"></x-forms.input>
|
||||
<x-forms.input canGate="update" :canResource="$database" label="Description" id="description"></x-forms.input>
|
||||
<x-forms.input canGate="update" :canResource="$database" required
|
||||
helper="You can change the image you would like to deploy.<br><br><span class='dark:text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"
|
||||
label="Image" id="image"></x-forms.input>
|
||||
</div>
|
||||
<div class="flex items-end gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$database" placeholder="5432" disabled="{{ $database->is_public }}" id="publicPort"
|
||||
label="Public Port" />
|
||||
<x-forms.checkbox canGate="update" :canResource="$database" instantSave id="isPublic" label="Make it publicly available" />
|
||||
</div>
|
||||
@if ($db_url_public)
|
||||
<x-forms.input label="Database IP:PORT (public)"
|
||||
helper="Your credentials are available in your environment variables." type="password" readonly
|
||||
wire:model="db_url_public" />
|
||||
@endif
|
||||
</div>
|
||||
<h3 class="pt-2">Advanced</h3>
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$database" instantSave="instantSaveExclude" label="Exclude from service status"
|
||||
helper="If you do not need to monitor this resource, enable. Useful if this service is optional."
|
||||
id="excludeFromStatus"></x-forms.checkbox>
|
||||
<x-forms.checkbox canGate="update" :canResource="$database" helper="Drain logs to your configured log drain endpoint in your Server settings."
|
||||
instantSave="instantSaveLogDrain" id="isLogDrainEnabled" label="Drain Logs" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@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)
|
||||
<div class="pt-6">
|
||||
<h3 class="pb-4">Import Backup</h3>
|
||||
<livewire:project.database.import :resource="$database" />
|
||||
</div>
|
||||
@endcan
|
||||
@endif
|
||||
</div>
|
||||
|
|
@ -1,54 +1,257 @@
|
|||
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }">
|
||||
<div>
|
||||
<livewire:project.service.heading :service="$service" :parameters="$parameters" :query="$query" />
|
||||
<div class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<div class="flex flex-col items-start gap-2 min-w-fit">
|
||||
<a class="menu-item"
|
||||
class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.configuration', [...$parameters, 'stack_service_uuid' => null]) }}">
|
||||
<button><- Back</button>
|
||||
</a>
|
||||
<a class="menu-item" :class="activeTab === 'general' && 'menu-item-active'"
|
||||
@click.prevent="activeTab = 'general'; window.location.hash = 'general'; if(window.location.search) window.location.search = ''"
|
||||
href="#">General</a>
|
||||
@if ($serviceDatabase?->isBackupSolutionAvailable() || $serviceDatabase?->is_migrated)
|
||||
<a :class="activeTab === 'backups' && 'menu-item-active'" class="menu-item"
|
||||
@click.prevent="activeTab = 'backups'; window.location.hash = 'backups'" href="#backups">Backups</a>
|
||||
@endif
|
||||
</div>
|
||||
@if ($resourceType === 'database')
|
||||
<x-service-database.sidebar :parameters="$parameters" :serviceDatabase="$serviceDatabase" :isImportSupported="$isImportSupported" />
|
||||
@else
|
||||
<div class="flex flex-col items-start gap-2 min-w-fit">
|
||||
<a class="menu-item"
|
||||
class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-active' : '' }}"
|
||||
{{ wireNavigate() }}
|
||||
href="{{ route('project.service.configuration', [...$parameters, 'stack_service_uuid' => null]) }}">
|
||||
<button><- Back</button>
|
||||
</a>
|
||||
<a class="menu-item menu-item-active" href="#">General</a>
|
||||
</div>
|
||||
@endif
|
||||
<div class="w-full">
|
||||
@isset($serviceApplication)
|
||||
@if ($resourceType === 'application')
|
||||
<x-slot:title>
|
||||
{{ data_get_str($service, 'name')->limit(10) }} >
|
||||
{{ data_get_str($serviceApplication, 'name')->limit(10) }} | Coolify
|
||||
</x-slot>
|
||||
<div x-cloak x-show="activeTab === 'general'" class="h-full">
|
||||
<livewire:project.service.service-application-view :application="$serviceApplication" />
|
||||
</div>
|
||||
@endisset
|
||||
@isset($serviceDatabase)
|
||||
<form wire:submit='submitApplication'>
|
||||
<div class="flex items-center gap-2 pb-4">
|
||||
@if ($serviceApplication->human_name)
|
||||
<h2>{{ Str::headline($serviceApplication->human_name) }}</h2>
|
||||
@else
|
||||
<h2>{{ Str::headline($serviceApplication->name) }}</h2>
|
||||
@endif
|
||||
<x-forms.button canGate="update" :canResource="$serviceApplication" type="submit">Save</x-forms.button>
|
||||
@can('update', $serviceApplication)
|
||||
<x-modal-confirmation wire:click="convertToDatabase" title="Convert to Database"
|
||||
buttonTitle="Convert to Database" submitAction="convertToDatabase" :actions="['The selected resource will be converted to a service database.']"
|
||||
confirmationText="{{ Str::headline($serviceApplication->name) }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Service Application Name below"
|
||||
shortConfirmationLabel="Service Application Name" />
|
||||
@endcan
|
||||
@can('delete', $serviceApplication)
|
||||
<x-modal-confirmation title="Confirm Service Application Deletion?" buttonTitle="Delete" isErrorButton
|
||||
submitAction="deleteApplication" :actions="['The selected service application container will be stopped and permanently deleted.']"
|
||||
confirmationText="{{ Str::headline($serviceApplication->name) }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Service Application Name below"
|
||||
shortConfirmationLabel="Service Application Name" />
|
||||
@endcan
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
@if ($requiredPort && !$serviceApplication->serviceType()?->contains(str($serviceApplication->image)->before(':')))
|
||||
<x-callout type="warning" title="Required Port: {{ $requiredPort }}" class="mb-2">
|
||||
This service requires port <strong>{{ $requiredPort }}</strong> to function correctly. All domains must include this port number (or any other port if you know what you're doing).
|
||||
<br><br>
|
||||
<strong>Example:</strong> http://app.coolify.io:{{ $requiredPort }}
|
||||
</x-callout>
|
||||
@endif
|
||||
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$serviceApplication" label="Name" id="humanName"
|
||||
placeholder="Human readable name"></x-forms.input>
|
||||
<x-forms.input canGate="update" :canResource="$serviceApplication" label="Description"
|
||||
id="description"></x-forms.input>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@if (!$serviceApplication->serviceType()?->contains(str($serviceApplication->image)->before(':')))
|
||||
@if ($serviceApplication->required_fqdn)
|
||||
<x-forms.input canGate="update" :canResource="$serviceApplication" required placeholder="https://app.coolify.io"
|
||||
label="Domains" id="fqdn"
|
||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
||||
@else
|
||||
<x-forms.input canGate="update" :canResource="$serviceApplication" placeholder="https://app.coolify.io"
|
||||
label="Domains" id="fqdn"
|
||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
||||
@endif
|
||||
@endif
|
||||
<x-forms.input canGate="update" :canResource="$serviceApplication"
|
||||
helper="You can change the image you would like to deploy.<br><br><span class='dark:text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"
|
||||
label="Image" id="image"></x-forms.input>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="py-2 pt-4">Advanced</h3>
|
||||
<div class="w-96 flex flex-col gap-1">
|
||||
@if (str($serviceApplication->image)->contains('pocketbase'))
|
||||
<x-forms.checkbox canGate="update" :canResource="$serviceApplication" instantSave="instantSaveApplicationSettings" id="isGzipEnabled"
|
||||
label="Enable Gzip Compression"
|
||||
helper="Pocketbase does not need gzip compression, otherwise SSE will not work." disabled />
|
||||
@else
|
||||
<x-forms.checkbox canGate="update" :canResource="$serviceApplication" instantSave="instantSaveApplicationSettings" id="isGzipEnabled"
|
||||
label="Enable Gzip Compression"
|
||||
helper="You can disable gzip compression if you want. Some services are compressing data by default. In this case, you do not need this." />
|
||||
@endif
|
||||
<x-forms.checkbox canGate="update" :canResource="$serviceApplication" instantSave="instantSaveApplicationSettings" id="isStripprefixEnabled"
|
||||
label="Strip Prefixes"
|
||||
helper="Strip Prefix is used to remove prefixes from paths. Like /api/ to /api." />
|
||||
<x-forms.checkbox canGate="update" :canResource="$serviceApplication" instantSave="instantSaveApplicationSettings" label="Exclude from service status"
|
||||
helper="If you do not need to monitor this resource, enable. Useful if this service is optional."
|
||||
id="excludeFromStatus"></x-forms.checkbox>
|
||||
<x-forms.checkbox canGate="update" :canResource="$serviceApplication"
|
||||
helper="Drain logs to your configured log drain endpoint in your Server settings."
|
||||
instantSave="instantSaveApplicationAdvanced" id="isLogDrainEnabled" label="Drain Logs" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<x-domain-conflict-modal
|
||||
:conflicts="$domainConflicts"
|
||||
:showModal="$showDomainConflictModal"
|
||||
confirmAction="confirmDomainUsage">
|
||||
<x-slot:consequences>
|
||||
<ul class="mt-2 ml-4 list-disc">
|
||||
<li>Only one service will be accessible at this domain</li>
|
||||
<li>The routing behavior will be unpredictable</li>
|
||||
<li>You may experience service disruptions</li>
|
||||
<li>SSL certificates might not work correctly</li>
|
||||
</ul>
|
||||
</x-slot:consequences>
|
||||
</x-domain-conflict-modal>
|
||||
|
||||
@if ($showPortWarningModal)
|
||||
<div x-data="{ modalOpen: true }" x-init="$nextTick(() => { modalOpen = true })"
|
||||
@keydown.escape.window="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
:class="{ 'z-40': modalOpen }" class="relative w-auto h-auto">
|
||||
<template x-teleport="body">
|
||||
<div x-show="modalOpen"
|
||||
class="fixed top-0 lg:pt-10 left-0 z-99 flex items-start justify-center w-screen h-screen" x-cloak>
|
||||
<div x-show="modalOpen" class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
|
||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave="ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||
class="relative w-full py-6 border rounded-sm min-w-full lg:min-w-[36rem] max-w-[48rem] bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300">
|
||||
<div class="flex justify-between items-center pb-3">
|
||||
<h2 class="pr-8 font-bold">Remove Required Port?</h2>
|
||||
<button @click="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
class="flex absolute top-2 right-2 justify-center items-center w-8 h-8 rounded-full dark:text-white hover:bg-coolgray-300">
|
||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative w-auto">
|
||||
<x-callout type="warning" title="Port Requirement Warning" class="mb-4">
|
||||
This service requires port <strong>{{ $requiredPort }}</strong> to function correctly.
|
||||
One or more of your domains are missing a port number.
|
||||
</x-callout>
|
||||
|
||||
<x-callout type="danger" title="What will happen if you continue?" class="mb-4">
|
||||
<ul class="mt-2 ml-4 list-disc">
|
||||
<li>The service may become unreachable</li>
|
||||
<li>The proxy may not be able to route traffic correctly</li>
|
||||
<li>Environment variables may not be generated properly</li>
|
||||
<li>The service may fail to start or function</li>
|
||||
</ul>
|
||||
</x-callout>
|
||||
|
||||
<div class="flex flex-wrap gap-2 justify-between mt-4">
|
||||
<x-forms.button @click="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
class="w-auto dark:bg-coolgray-200 dark:hover:bg-coolgray-300">
|
||||
Cancel - Keep Port
|
||||
</x-forms.button>
|
||||
<x-forms.button wire:click="confirmRemovePort" @click="modalOpen = false" class="w-auto"
|
||||
isError>
|
||||
I understand, remove port anyway
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@endif
|
||||
@elseif ($resourceType === 'database')
|
||||
<x-slot:title>
|
||||
{{ data_get_str($service, 'name')->limit(10) }} >
|
||||
{{ data_get_str($serviceDatabase, 'name')->limit(10) }} | Coolify
|
||||
</x-slot>
|
||||
<div x-cloak x-show="activeTab === 'general'" class="h-full">
|
||||
<livewire:project.service.database :database="$serviceDatabase" />
|
||||
</div>
|
||||
@if ($serviceDatabase?->isBackupSolutionAvailable() || $serviceDatabase?->is_migrated)
|
||||
<div x-cloak x-show="activeTab === 'backups'">
|
||||
<div class="flex gap-2">
|
||||
<h2 class="pb-4">Scheduled Backups</h2>
|
||||
@if (filled($serviceDatabase->custom_type) || !$serviceDatabase->is_migrated)
|
||||
@can('update', $serviceDatabase)
|
||||
<x-modal-input buttonTitle="+ Add" title="New Scheduled Backup">
|
||||
<livewire:project.database.create-scheduled-backup :database="$serviceDatabase" />
|
||||
</x-modal-input>
|
||||
@endcan
|
||||
@if ($currentRoute === 'project.service.database.import')
|
||||
<livewire:project.database.import :resource="$serviceDatabase" :key="'import-' . $serviceDatabase->uuid" />
|
||||
@else
|
||||
<form wire:submit='submitDatabase'>
|
||||
<div class="flex items-center gap-2 pb-4">
|
||||
@if ($serviceDatabase->human_name)
|
||||
<h2>{{ Str::headline($serviceDatabase->human_name) }}</h2>
|
||||
@else
|
||||
<h2>{{ Str::headline($serviceDatabase->name) }}</h2>
|
||||
@endif
|
||||
<x-forms.button canGate="update" :canResource="$serviceDatabase" type="submit">Save</x-forms.button>
|
||||
@can('update', $serviceDatabase)
|
||||
<x-modal-confirmation wire:click="convertToApplication" title="Convert to Application"
|
||||
buttonTitle="Convert to Application" submitAction="convertToApplication" :actions="['The selected resource will be converted to an application.']"
|
||||
confirmationText="{{ Str::headline($serviceDatabase->name) }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Service Database Name below"
|
||||
shortConfirmationLabel="Service Database Name" />
|
||||
@endcan
|
||||
@can('delete', $serviceDatabase)
|
||||
<x-modal-confirmation title="Confirm Service Database Deletion?" buttonTitle="Delete"
|
||||
isErrorButton submitAction="deleteDatabase" :actions="[
|
||||
'The selected service database container will be stopped and permanently deleted.',
|
||||
]"
|
||||
confirmationText="{{ Str::headline($serviceDatabase->name) }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Service Database Name below"
|
||||
shortConfirmationLabel="Service Database Name" />
|
||||
@endcan
|
||||
</div>
|
||||
<livewire:project.database.scheduled-backups :database="$serviceDatabase" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$serviceDatabase" label="Name" id="humanName"
|
||||
placeholder="Name"></x-forms.input>
|
||||
<x-forms.input canGate="update" :canResource="$serviceDatabase" label="Description"
|
||||
id="description"></x-forms.input>
|
||||
<x-forms.input canGate="update" :canResource="$serviceDatabase" required
|
||||
helper="You can change the image you would like to deploy.<br><br><span class='dark:text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"
|
||||
label="Image" id="image"></x-forms.input>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2 py-2">
|
||||
<h3>Proxy</h3>
|
||||
<x-loading wire:loading wire:target="instantSave" />
|
||||
@if ($serviceDatabase->is_public)
|
||||
<x-slide-over fullScreen>
|
||||
<x-slot:title>Proxy Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:project.shared.get-logs :server="$server" :resource="$service"
|
||||
:servicesubtype="$serviceDatabase" container="{{ $serviceDatabase->uuid }}-proxy" :collapsible="false" lazy />
|
||||
</x-slot:content>
|
||||
<x-forms.button @click="slideOverOpen=true">Logs</x-forms.button>
|
||||
</x-slide-over>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 w-64">
|
||||
<x-forms.checkbox canGate="update" :canResource="$serviceDatabase" instantSave id="isPublic"
|
||||
label="Make it publicly available" />
|
||||
</div>
|
||||
<x-forms.input canGate="update" :canResource="$serviceDatabase" placeholder="5432"
|
||||
disabled="{{ $serviceDatabase->is_public }}" id="publicPort" label="Public Port" />
|
||||
@if ($db_url_public)
|
||||
<x-forms.input label="Database IP:PORT (public)"
|
||||
helper="Your credentials are available in your environment variables." type="password"
|
||||
readonly wire:model="db_url_public" />
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="pt-2">Advanced</h3>
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$serviceDatabase" instantSave="instantSaveExclude"
|
||||
label="Exclude from service status"
|
||||
helper="If you do not need to monitor this resource, enable. Useful if this service is optional."
|
||||
id="excludeFromStatus"></x-forms.checkbox>
|
||||
<x-forms.checkbox canGate="update" :canResource="$serviceDatabase"
|
||||
helper="Drain logs to your configured log drain endpoint in your Server settings."
|
||||
instantSave="instantSaveLogDrain" id="isLogDrainEnabled" label="Drain Logs" />
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
@endisset
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,149 +0,0 @@
|
|||
<div>
|
||||
<form wire:submit='submit'>
|
||||
<div class="flex items-center gap-2 pb-4">
|
||||
@if ($application->human_name)
|
||||
<h2>{{ Str::headline($application->human_name) }}</h2>
|
||||
@else
|
||||
<h2>{{ Str::headline($application->name) }}</h2>
|
||||
@endif
|
||||
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
|
||||
@can('update', $application)
|
||||
<x-modal-confirmation wire:click="convertToDatabase" title="Convert to Database"
|
||||
buttonTitle="Convert to Database" submitAction="convertToDatabase" :actions="['The selected resource will be converted to a service database.']"
|
||||
confirmationText="{{ Str::headline($application->name) }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Service Application Name below"
|
||||
shortConfirmationLabel="Service Application Name" />
|
||||
@endcan
|
||||
@can('delete', $application)
|
||||
<x-modal-confirmation title="Confirm Service Application Deletion?" buttonTitle="Delete" isErrorButton
|
||||
submitAction="delete" :actions="['The selected service application container will be stopped and permanently deleted.']" confirmationText="{{ Str::headline($application->name) }}"
|
||||
confirmationLabel="Please confirm the execution of the actions by entering the Service Application Name below"
|
||||
shortConfirmationLabel="Service Application Name" />
|
||||
@endcan
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
@if($requiredPort && !$application->serviceType()?->contains(str($application->image)->before(':')))
|
||||
<x-callout type="warning" title="Required Port: {{ $requiredPort }}" class="mb-2">
|
||||
This service requires port <strong>{{ $requiredPort }}</strong> to function correctly. All domains must include this port number (or any other port if you know what you're doing).
|
||||
<br><br>
|
||||
<strong>Example:</strong> http://app.coolify.io:{{ $requiredPort }}
|
||||
</x-callout>
|
||||
@endif
|
||||
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$application" label="Name" id="humanName"
|
||||
placeholder="Human readable name"></x-forms.input>
|
||||
<x-forms.input canGate="update" :canResource="$application" label="Description"
|
||||
id="description"></x-forms.input>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@if (!$application->serviceType()?->contains(str($application->image)->before(':')))
|
||||
@if ($application->required_fqdn)
|
||||
<x-forms.input canGate="update" :canResource="$application" required placeholder="https://app.coolify.io"
|
||||
label="Domains" id="fqdn"
|
||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
||||
@else
|
||||
<x-forms.input canGate="update" :canResource="$application" placeholder="https://app.coolify.io"
|
||||
label="Domains" id="fqdn"
|
||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
||||
@endif
|
||||
@endif
|
||||
<x-forms.input canGate="update" :canResource="$application"
|
||||
helper="You can change the image you would like to deploy.<br><br><span class='dark:text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"
|
||||
label="Image" id="image"></x-forms.input>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="py-2 pt-4">Advanced</h3>
|
||||
<div class="w-96 flex flex-col gap-1">
|
||||
@if (str($application->image)->contains('pocketbase'))
|
||||
<x-forms.checkbox canGate="update" :canResource="$application" instantSave="instantSaveSettings" id="isGzipEnabled"
|
||||
label="Enable Gzip Compression"
|
||||
helper="Pocketbase does not need gzip compression, otherwise SSE will not work." disabled />
|
||||
@else
|
||||
<x-forms.checkbox canGate="update" :canResource="$application" instantSave="instantSaveSettings" id="isGzipEnabled"
|
||||
label="Enable Gzip Compression"
|
||||
helper="You can disable gzip compression if you want. Some services are compressing data by default. In this case, you do not need this." />
|
||||
@endif
|
||||
<x-forms.checkbox canGate="update" :canResource="$application" instantSave="instantSaveSettings" id="isStripprefixEnabled"
|
||||
label="Strip Prefixes"
|
||||
helper="Strip Prefix is used to remove prefixes from paths. Like /api/ to /api." />
|
||||
<x-forms.checkbox canGate="update" :canResource="$application" instantSave="instantSaveSettings" label="Exclude from service status"
|
||||
helper="If you do not need to monitor this resource, enable. Useful if this service is optional."
|
||||
id="excludeFromStatus"></x-forms.checkbox>
|
||||
<x-forms.checkbox canGate="update" :canResource="$application"
|
||||
helper="Drain logs to your configured log drain endpoint in your Server settings."
|
||||
instantSave="instantSaveAdvanced" id="isLogDrainEnabled" label="Drain Logs" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<x-domain-conflict-modal
|
||||
:conflicts="$domainConflicts"
|
||||
:showModal="$showDomainConflictModal"
|
||||
confirmAction="confirmDomainUsage">
|
||||
<x-slot:consequences>
|
||||
<ul class="mt-2 ml-4 list-disc">
|
||||
<li>Only one service will be accessible at this domain</li>
|
||||
<li>The routing behavior will be unpredictable</li>
|
||||
<li>You may experience service disruptions</li>
|
||||
<li>SSL certificates might not work correctly</li>
|
||||
</ul>
|
||||
</x-slot:consequences>
|
||||
</x-domain-conflict-modal>
|
||||
|
||||
@if ($showPortWarningModal)
|
||||
<div x-data="{ modalOpen: true }" x-init="$nextTick(() => { modalOpen = true })"
|
||||
@keydown.escape.window="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
:class="{ 'z-40': modalOpen }" class="relative w-auto h-auto">
|
||||
<template x-teleport="body">
|
||||
<div x-show="modalOpen"
|
||||
class="fixed top-0 lg:pt-10 left-0 z-99 flex items-start justify-center w-screen h-screen" x-cloak>
|
||||
<div x-show="modalOpen" class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
|
||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave="ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||
class="relative w-full py-6 border rounded-sm min-w-full lg:min-w-[36rem] max-w-[48rem] bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300">
|
||||
<div class="flex justify-between items-center pb-3">
|
||||
<h2 class="pr-8 font-bold">Remove Required Port?</h2>
|
||||
<button @click="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
class="flex absolute top-2 right-2 justify-center items-center w-8 h-8 rounded-full dark:text-white hover:bg-coolgray-300">
|
||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative w-auto">
|
||||
<x-callout type="warning" title="Port Requirement Warning" class="mb-4">
|
||||
This service requires port <strong>{{ $requiredPort }}</strong> to function correctly.
|
||||
One or more of your domains are missing a port number.
|
||||
</x-callout>
|
||||
|
||||
<x-callout type="danger" title="What will happen if you continue?" class="mb-4">
|
||||
<ul class="mt-2 ml-4 list-disc">
|
||||
<li>The service may become unreachable</li>
|
||||
<li>The proxy may not be able to route traffic correctly</li>
|
||||
<li>Environment variables may not be generated properly</li>
|
||||
<li>The service may fail to start or function</li>
|
||||
</ul>
|
||||
</x-callout>
|
||||
|
||||
<div class="flex flex-wrap gap-2 justify-between mt-4">
|
||||
<x-forms.button @click="modalOpen = false; $wire.call('cancelRemovePort')"
|
||||
class="w-auto dark:bg-coolgray-200 dark:hover:bg-coolgray-300">
|
||||
Cancel - Keep Port
|
||||
</x-forms.button>
|
||||
<x-forms.button wire:click="confirmRemovePort" @click="modalOpen = false" class="w-auto"
|
||||
isError>
|
||||
I understand, remove port anyway
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue