Compare commits

...

36 Commits

Author SHA1 Message Date
Andras Bacsai
642a6e3203 Merge pull request #1721 from coollabsio/quick-fix
Update database/service start commands
2024-02-07 20:34:58 +01:00
Andras Bacsai
9edbc15828 Update database start commands 2024-02-07 20:34:13 +01:00
Andras Bacsai
dadc7aaf08 Merge pull request #1718 from coollabsio/next
v4.0.0-beta.210
2024-02-06 11:41:17 +01:00
Andras Bacsai
b96807d34c fix: feedback from self-hosted envs to discord 2024-02-06 11:36:20 +01:00
Andras Bacsai
45b736bb01 fix: stripe webhooks 2024-02-06 11:11:26 +01:00
Andras Bacsai
3d873a79a0 Merge pull request #1715 from coollabsio/next
fix: deploy issue with tag deployment
2024-02-06 07:21:33 +01:00
Andras Bacsai
6869c582ff Update retrieval of applications and services in Deploy controller 2024-02-06 07:21:06 +01:00
Andras Bacsai
9b9e5e939c Fix resource not found error and improve mass deployment process 2024-02-06 07:19:11 +01:00
Andras Bacsai
f626c15ecc Update version numbers + fix deploy issue 2024-02-06 07:12:09 +01:00
Andras Bacsai
8df1fe2e60 Merge pull request #1714 from coollabsio/next
Refactor database and service start commands
2024-02-05 20:58:16 +01:00
Andras Bacsai
fd2a533057 Refactor database and service start commands 2024-02-05 20:57:40 +01:00
Andras Bacsai
0a6401f990 Merge pull request #1713 from coollabsio/next
v4.0.0-beta.208
2024-02-05 20:24:44 +01:00
Andras Bacsai
1326fcb345 Add count checks for MySQL and MariaDB in isEmpty() method 2024-02-05 20:15:02 +01:00
Andras Bacsai
8b8e534598 Update version numbers to 4.0.0-beta.208 2024-02-05 19:53:14 +01:00
Andras Bacsai
0b518a3b76 Refactor code to load tags for environment applications and databases 2024-02-05 19:52:06 +01:00
Andras Bacsai
93fb14884e Merge pull request #1711 from coollabsio/next
Refactor server validation and installation logic
2024-02-05 15:13:56 +01:00
Andras Bacsai
26ccc4afb4 Refactor server validation and installation logic 2024-02-05 15:13:39 +01:00
Andras Bacsai
5fda1bb932 Merge pull request #1710 from coollabsio/next
v4.0.0-beta.207
2024-02-05 14:57:21 +01:00
Andras Bacsai
409ba8a1bb Refactor application deployment logic 2024-02-05 14:47:06 +01:00
Andras Bacsai
49f5240ff8 fix: better server validation and installation process
fix: add destination to queue deployment
feat: force start deployment
2024-02-05 14:40:54 +01:00
Andras Bacsai
0c3ed3d393 Update BunnyCDN sync and version numbers 2024-02-05 10:17:40 +01:00
Andras Bacsai
6e3dc474f2 Merge pull request #1702 from coollabsio/next
v4.0.0-beta.206
2024-02-05 10:06:59 +01:00
Andras Bacsai
d3eb87561e Fix styling issue in tag links 2024-02-05 10:00:53 +01:00
Andras Bacsai
8b58c8f856 Add tags to show and index views 2024-02-05 09:51:44 +01:00
Andras Bacsai
8c60ef5bd6 Update link in error message to the correct documentation 2024-02-04 17:00:13 +01:00
Andras Bacsai
1d59383c78 feat: clone to env 2024-02-04 16:54:12 +01:00
Andras Bacsai
60f590454d Update application deployment status in job handling 2024-02-04 14:40:23 +01:00
Andras Bacsai
dcb61a553e Merge pull request #1706 from piscis/patch-1
fix: Wrap tags and avoid horizontal overflow
2024-02-04 14:39:55 +01:00
Andras Bacsai
e06e31642f Refactor modal component and add new functionality 2024-02-04 14:07:08 +01:00
Andras Bacsai
9dfce48380 Add private_keys array initialization and define additional private properties 2024-02-04 13:50:24 +01:00
Andras Bacsai
8eed87e2f7 Update main class with mx-auto 2024-02-04 13:50:16 +01:00
Alex
d56d4eb8fc fix: Wrap tags and avoid horizontal overflow 2024-02-04 13:15:39 +01:00
Andras Bacsai
fd32cd04ab Refactor invoice payment failure handling in webhooks.php 2024-02-04 12:23:00 +01:00
Andras Bacsai
1d3b7ffd3b Refactor tags functionality and improve user experience 2024-02-03 12:44:18 +01:00
Andras Bacsai
0b5baf60a5 fix: tags 2024-02-03 12:39:07 +01:00
Andras Bacsai
bc31df6fb2 Update version numbers to 4.0.0-beta.206 2024-02-02 14:52:24 +01:00
75 changed files with 811 additions and 363 deletions

View File

@@ -106,7 +106,8 @@ class StartMariadb
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '{$database->name} started.'"; $database_name = addslashes($database->name);
$this->commands[] = "echo 'Database started.'";
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged'); return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
} }

View File

@@ -122,7 +122,8 @@ class StartMongodb
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '{$database->name} started.'"; $database_name = addslashes($database->name);
$this->commands[] = "echo 'Database started.'";
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged'); return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
} }

View File

@@ -106,7 +106,8 @@ class StartMysql
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '{$database->name} started.'"; $database_name = addslashes($database->name);
$this->commands[] = "echo 'Database started.'";
return remote_process($this->commands, $database->destination->server,callEventOnFinish: 'DatabaseStatusChanged'); return remote_process($this->commands, $database->destination->server,callEventOnFinish: 'DatabaseStatusChanged');
} }

View File

@@ -128,7 +128,8 @@ class StartPostgresql
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '{$database->name} started.'"; $database_name = addslashes($database->name);
$this->commands[] = "echo 'Database started.'";
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged'); return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
} }

View File

@@ -117,7 +117,8 @@ class StartRedis
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '{$database->name} started.'"; $database_name = addslashes($database->name);
$this->commands[] = "echo 'Database started.'";
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged'); return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
} }

View File

@@ -43,6 +43,7 @@ class InstallDocker
"echo 'Restarting Docker Engine...'", "echo 'Restarting Docker Engine...'",
"ls -l /tmp" "ls -l /tmp"
]); ]);
return remote_process($command, $server);
} else { } else {
if ($supported_os_type->contains('debian')) { if ($supported_os_type->contains('debian')) {
$command = $command->merge([ $command = $command->merge([
@@ -89,7 +90,6 @@ class InstallDocker
"echo 'Done!'", "echo 'Done!'",
]); ]);
} }
return remote_process($command, $server); return remote_process($command, $server);
} }
} }

View File

@@ -13,11 +13,15 @@ class StartService
{ {
ray('Starting service: ' . $service->name); ray('Starting service: ' . $service->name);
$service->saveComposeConfigs(); $service->saveComposeConfigs();
$service_name = addslashes($service->name);
$server_name = addslashes($service->server->name);
$commands[] = "cd " . $service->workdir(); $commands[] = "cd " . $service->workdir();
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'"; $commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
$commands[] = "echo 'Creating Docker network.'"; $commands[] = "echo 'Creating Docker network.'";
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid >/dev/null 2>&1 || true"; $commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid >/dev/null 2>&1 || true";
$commands[] = "echo 'Starting service $service->name on {$service->server->name}.'"; $commands[] = "echo Starting service.";
$commands[] = "echo 'Pulling images.'"; $commands[] = "echo 'Pulling images.'";
$commands[] = "docker compose pull"; $commands[] = "docker compose pull";
$commands[] = "echo 'Starting containers.'"; $commands[] = "echo 'Starting containers.'";

View File

@@ -48,7 +48,7 @@ class SyncBunny extends Command
$versions = "versions.json"; $versions = "versions.json";
PendingRequest::macro('storage', function ($fileName) use($that) { PendingRequest::macro('storage', function ($fileName) use ($that) {
$headers = [ $headers = [
'AccessKey' => env('BUNNY_STORAGE_API_KEY'), 'AccessKey' => env('BUNNY_STORAGE_API_KEY'),
'Accept' => 'application/json', 'Accept' => 'application/json',
@@ -76,23 +76,26 @@ class SyncBunny extends Command
} }
if ($only_template) { if ($only_template) {
$this->info('About to sync service-templates.json to BunnyCDN.'); $this->info('About to sync service-templates.json to BunnyCDN.');
} $confirmed = confirm("Are you sure you want to sync?");
if ($only_version) { if (!$confirmed) {
$this->info('About to sync versions.json to BunnyCDN.'); return;
} }
$confirmed = confirm('Are you sure you want to sync?');
if (!$confirmed) {
return;
}
if ($only_template) {
Http::pool(fn (Pool $pool) => [ Http::pool(fn (Pool $pool) => [
$pool->storage(fileName: "$parent_dir/templates/$service_template")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$service_template"), $pool->storage(fileName: "$parent_dir/templates/$service_template")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$service_template"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$service_template"), $pool->purge("$bunny_cdn/$bunny_cdn_path/$service_template"),
]); ]);
$this->info('Service template uploaded & purged...'); $this->info('Service template uploaded & purged...');
return; return;
} } else if ($only_version) {
if ($only_version) { $this->info('About to sync versions.json to BunnyCDN.');
$file = file_get_contents("$parent_dir/$versions");
$json = json_decode($file, true);
$actual_version = data_get($json, 'coolify.v4.version');
$confirmed = confirm("Are you sure you want to sync to {$actual_version}?");
if (!$confirmed) {
return;
}
Http::pool(fn (Pool $pool) => [ Http::pool(fn (Pool $pool) => [
$pool->storage(fileName: "$parent_dir/$versions")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"), $pool->storage(fileName: "$parent_dir/$versions")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"), $pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"),
@@ -101,6 +104,7 @@ class SyncBunny extends Command
return; return;
} }
Http::pool(fn (Pool $pool) => [ Http::pool(fn (Pool $pool) => [
$pool->storage(fileName: "$parent_dir/$compose_file")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"), $pool->storage(fileName: "$parent_dir/$compose_file")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"),
$pool->storage(fileName: "$parent_dir/$compose_file_prod")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"), $pool->storage(fileName: "$parent_dir/$compose_file_prod")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"),

View File

@@ -73,12 +73,17 @@ class Deploy extends Controller
$message->push("Tag {$tag} not found."); $message->push("Tag {$tag} not found.");
continue; continue;
} }
$resources = $found_tag->resources()->get(); $applications = $found_tag->applications()->get();
if ($resources->count() === 0) { $services = $found_tag->services()->get();
if ($applications->count() === 0 && $services->count() === 0) {
$message->push("No resources found for tag {$tag}."); $message->push("No resources found for tag {$tag}.");
continue; continue;
} }
foreach ($resources as $resource) { foreach ($applications as $resource) {
$return_message = $this->deploy_resource($resource, $force);
$message = $message->merge($return_message);
}
foreach ($services as $resource) {
$return_message = $this->deploy_resource($resource, $force); $return_message = $this->deploy_resource($resource, $force);
$message = $message->merge($return_message); $message = $message->merge($return_message);
} }
@@ -92,7 +97,10 @@ class Deploy extends Controller
public function deploy_resource($resource, bool $force = false): Collection public function deploy_resource($resource, bool $force = false): Collection
{ {
$message = collect([]); $message = collect([]);
$type = $resource->getMorphClass(); if (gettype($resource) !== 'object') {
return $message->push("Resource ($resource) not found.");
}
$type = $resource?->getMorphClass();
if ($type === 'App\Models\Application') { if ($type === 'App\Models\Application') {
queue_application_deployment( queue_application_deployment(
application: $resource, application: $resource,

View File

@@ -122,7 +122,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($source) { if ($source) {
$this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first(); $this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first();
} }
$this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first(); $this->server = Server::find($this->application_deployment_queue->server_id);
$this->destination = $this->server->destinations()->where('id', $this->application_deployment_queue->destination_id)->first();
$this->server = $this->mainServer = $this->destination->server; $this->server = $this->mainServer = $this->destination->server;
$this->serverUser = $this->server->user; $this->serverUser = $this->server->user;
$this->basedir = $this->application->generateBaseDir($this->deployment_uuid); $this->basedir = $this->application->generateBaseDir($this->deployment_uuid);
@@ -166,10 +167,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
public function handle(): void public function handle(): void
{ {
$this->application_deployment_queue->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
// Generate custom host<->ip mapping // Generate custom host<->ip mapping
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server); $allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
if (!is_null($allContainers)) { if (!is_null($allContainers)) {
@@ -565,12 +562,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->generate_build_env_variables(); $this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile(); $this->add_build_env_variables_to_dockerfile();
$this->build_image(); $this->build_image();
// if ($this->application->additional_destinations) {
// $this->push_to_docker_registry();
// $this->deploy_to_additional_destinations();
// } else {
$this->rolling_update(); $this->rolling_update();
// }
} }
private function deploy_nixpacks_buildpack() private function deploy_nixpacks_buildpack()
{ {
@@ -795,7 +787,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
private function deploy_to_additional_destinations() private function deploy_to_additional_destinations()
{ {
if (str($this->application->additional_destinations)->isEmpty()) {
return;
}
$destination_ids = collect(str($this->application->additional_destinations)->explode(',')); $destination_ids = collect(str($this->application->additional_destinations)->explode(','));
if ($this->server->isSwarm()) {
$this->application_deployment_queue->addLogEntry("Additional destinations are not supported in swarm mode.");
return;
}
if ($destination_ids->contains($this->destination->id)) {
ray('Same destination found in additional destinations. Skipping.');
return;
}
foreach ($destination_ids as $destination_id) { foreach ($destination_ids as $destination_id) {
$destination = StandaloneDocker::find($destination_id); $destination = StandaloneDocker::find($destination_id);
$server = $destination->server; $server = $destination->server;
@@ -803,11 +806,21 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->application_deployment_queue->addLogEntry("Skipping deployment to {$server->name}. Not in the same team?!"); $this->application_deployment_queue->addLogEntry("Skipping deployment to {$server->name}. Not in the same team?!");
continue; continue;
} }
$this->server = $server; // ray('Deploying to additional destination: ', $server->name);
$this->application_deployment_queue->addLogEntry("Deploying to {$this->server->name}."); $deployment_uuid = new Cuid2();
$this->prepare_builder_image(); queue_application_deployment(
$this->generate_image_names(); deployment_uuid: $deployment_uuid,
$this->rolling_update(); application: $this->application,
server: $server,
destination: $destination,
no_questions_asked: true,
);
$this->application_deployment_queue->addLogEntry("Deploying to additional server: {$server->name}. Click here to see the deployment status: " . route('project.application.deployment.show', [
'project_uuid' => data_get($this->application, 'environment.project.uuid'),
'application_uuid' => data_get($this->application, 'uuid'),
'deployment_uuid' => $deployment_uuid,
'environment_name' => data_get($this->application, 'environment.name'),
]));
} }
} }
private function set_base_dir() private function set_base_dir()
@@ -1511,11 +1524,13 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
'status' => $status, 'status' => $status,
]); ]);
} }
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
}
if ($status === ApplicationDeploymentStatus::FAILED->value) { if ($status === ApplicationDeploymentStatus::FAILED->value) {
$this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview)); $this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
return;
}
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
// $this->deploy_to_additional_destinations();
$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
} }
} }

View File

@@ -15,7 +15,7 @@ class ActivityMonitor extends Component
public $isPollingActive = false; public $isPollingActive = false;
protected $activity; protected $activity;
protected $listeners = ['newMonitorActivity']; protected $listeners = ['activityMonitor' => 'newMonitorActivity'];
public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished') public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished')
{ {

View File

@@ -12,6 +12,7 @@ use Livewire\Component;
class Index extends Component class Index extends Component
{ {
protected $listeners = ['serverInstalled' => 'validateServer'];
public string $currentState = 'welcome'; public string $currentState = 'welcome';
public ?string $selectedServerType = null; public ?string $selectedServerType = null;
@@ -93,7 +94,11 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->serverPublicKey = $this->createdServer->privateKey->publicKey(); $this->serverPublicKey = $this->createdServer->privateKey->publicKey();
return $this->validateServer('localhost'); return $this->validateServer('localhost');
} elseif ($this->selectedServerType === 'remote') { } elseif ($this->selectedServerType === 'remote') {
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get(); if (isDev()) {
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->get();
} else {
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
}
if ($this->privateKeys->count() > 0) { if ($this->privateKeys->count() > 0) {
$this->selectedExistingPrivateKey = $this->privateKeys->first()->id; $this->selectedExistingPrivateKey = $this->privateKeys->first()->id;
} }
@@ -190,6 +195,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->createdServer->addInitialNetwork(); $this->createdServer->addInitialNetwork();
$this->validateServer(); $this->validateServer();
} }
public function installServer()
{
$this->dispatch('validateServer', true);
}
public function validateServer() public function validateServer()
{ {
try { try {
@@ -228,7 +237,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->dockerInstallationStarted = true; $this->dockerInstallationStarted = true;
$activity = InstallDocker::run($this->createdServer); $activity = InstallDocker::run($this->createdServer);
$this->dispatch('installDocker'); $this->dispatch('installDocker');
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->dockerInstallationStarted = false; $this->dockerInstallationStarted = false;
return handleError(error: $e, livewire: $this); return handleError(error: $e, livewire: $this);

View File

@@ -2,8 +2,10 @@
namespace App\Livewire; namespace App\Livewire;
use App\Models\InstanceSettings;
use DanHarrin\LivewireRateLimiting\WithRateLimiting; use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Livewire\Component; use Livewire\Component;
@@ -28,9 +30,8 @@ class Help extends Component
public function submit() public function submit()
{ {
try { try {
$this->rateLimit(3, 60); $this->rateLimit(3, 30);
$this->validate(); $this->validate();
$subscriptionType = auth()->user()?->subscription?->type() ?? 'Free';
$debug = "Route: {$this->path}"; $debug = "Route: {$this->path}";
$mail = new MailMessage(); $mail = new MailMessage();
$mail->view( $mail->view(
@@ -40,9 +41,21 @@ class Help extends Component
'debug' => $debug 'debug' => $debug
] ]
); );
$mail->subject("[HELP - {$subscriptionType}]: {$this->subject}"); $mail->subject("[HELP]: {$this->subject}");
send_user_an_email($mail, auth()->user()?->email, 'hi@coollabs.io'); $settings = InstanceSettings::get();
$this->dispatch('success', 'Your message has been sent successfully. <br>We will get in touch with you as soon as possible.'); $type = set_transanctional_email_settings($settings);
if (!$type) {
$url = "https://app.coolify.io/api/feedback";
if (isDev()) {
$url = "http://localhost:80/api/feedback";
}
Http::post($url, [
'content' => "User: `" . auth()->user()?->email . "` with subject: `" . $this->subject . "` has the following problem: `" . $this->description . "`"
]);
} else {
send_user_an_email($mail, auth()->user()?->email, 'hi@coollabs.io');
}
$this->dispatch('success', 'Feedback sent.', 'We will get in touch with you as soon as possible.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Livewire;
use App\Models\User;
use Livewire\Component;
use Spatie\Activitylog\Models\Activity;
class NewActivityMonitor extends Component
{
public ?string $header = null;
public $activityId;
public $eventToDispatch = 'activityFinished';
public $isPollingActive = false;
protected $activity;
protected $listeners = ['newActivityMonitor' => 'newMonitorActivity'];
public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished')
{
$this->activityId = $activityId;
$this->eventToDispatch = $eventToDispatch;
$this->hydrateActivity();
$this->isPollingActive = true;
}
public function hydrateActivity()
{
$this->activity = Activity::find($this->activityId);
}
public function polling()
{
$this->hydrateActivity();
// $this->setStatus(ProcessStatus::IN_PROGRESS);
$exit_code = data_get($this->activity, 'properties.exitCode');
if ($exit_code !== null) {
// if ($exit_code === 0) {
// // $this->setStatus(ProcessStatus::FINISHED);
// } else {
// // $this->setStatus(ProcessStatus::ERROR);
// }
$this->isPollingActive = false;
if ($this->eventToDispatch !== null) {
if (str($this->eventToDispatch)->startsWith('App\\Events\\')) {
$causer_id = data_get($this->activity, 'causer_id');
$user = User::find($causer_id);
if ($user) {
foreach ($user->teams as $team) {
$teamId = $team->id;
$this->eventToDispatch::dispatch($teamId);
}
}
return;
}
$this->dispatch($this->eventToDispatch);
ray('Dispatched event: ' . $this->eventToDispatch);
}
}
}
}

View File

@@ -7,8 +7,6 @@ use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\Server; use App\Models\Server;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;
use Livewire\Component; use Livewire\Component;
class DeploymentNavbar extends Component class DeploymentNavbar extends Component
@@ -37,7 +35,15 @@ class DeploymentNavbar extends Component
$this->is_debug_enabled = $this->application->settings->is_debug_enabled; $this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->dispatch('refreshQueue'); $this->dispatch('refreshQueue');
} }
public function force_start()
{
try {
force_start_deployment($this->application_deployment_queue);
} catch (\Throwable $e) {
ray($e);
return handleError($e, $this);
}
}
public function cancel() public function cancel()
{ {
try { try {
@@ -67,7 +73,6 @@ class DeploymentNavbar extends Component
'current_process_id' => null, 'current_process_id' => null,
'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value, 'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
]); ]);
// queue_next_deployment($this->application);
} }
} }
} }

View File

@@ -46,26 +46,6 @@ class Heading extends Component
$this->deploy(force_rebuild: true); $this->deploy(force_rebuild: true);
} }
public function deployNew()
{
if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {
$this->dispatch('error', 'Please load a Compose file first.');
return;
}
$this->setDeploymentUuid();
queue_application_deployment(
application: $this->application,
deployment_uuid: $this->deploymentUuid,
force_rebuild: false,
is_new_deployment: true,
);
return redirect()->route('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_name' => $this->parameters['environment_name'],
]);
}
public function deploy(bool $force_rebuild = false) public function deploy(bool $force_rebuild = false)
{ {
if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) { if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {

View File

@@ -22,12 +22,12 @@ class CloneMe extends Component
public ?int $selectedDestination = null; public ?int $selectedDestination = null;
public ?Server $server = null; public ?Server $server = null;
public $resources = []; public $resources = [];
public string $newProjectName = ''; public string $newName = '';
protected $messages = [ protected $messages = [
'selectedServer' => 'Please select a server.', 'selectedServer' => 'Please select a server.',
'selectedDestination' => 'Please select a server & destination.', 'selectedDestination' => 'Please select a server & destination.',
'newProjectName' => 'Please enter a name for the new project.', 'newName' => 'Please enter a name for the new project or environment.',
]; ];
public function mount($project_uuid) public function mount($project_uuid)
{ {
@@ -36,7 +36,7 @@ class CloneMe extends Component
$this->environment = $this->project->environments->where('name', $this->environment_name)->first(); $this->environment = $this->project->environments->where('name', $this->environment_name)->first();
$this->project_id = $this->project->id; $this->project_id = $this->project->id;
$this->servers = currentTeam()->servers; $this->servers = currentTeam()->servers;
$this->newProjectName = str($this->project->name . '-clone-' . (string)new Cuid2(7))->slug(); $this->newName = str($this->project->name . '-clone-' . (string)new Cuid2(7))->slug();
} }
public function render() public function render()
@@ -46,34 +46,50 @@ class CloneMe extends Component
public function selectServer($server_id, $destination_id) public function selectServer($server_id, $destination_id)
{ {
if ($server_id == $this->selectedServer && $destination_id == $this->selectedDestination) {
$this->selectedServer = null;
$this->selectedDestination = null;
$this->server = null;
return;
}
$this->selectedServer = $server_id; $this->selectedServer = $server_id;
$this->selectedDestination = $destination_id; $this->selectedDestination = $destination_id;
$this->server = $this->servers->where('id', $server_id)->first(); $this->server = $this->servers->where('id', $server_id)->first();
} }
public function clone() public function clone(string $type)
{ {
try { try {
$this->validate([ $this->validate([
'selectedDestination' => 'required', 'selectedDestination' => 'required',
'newProjectName' => 'required', 'newName' => 'required',
]); ]);
$foundProject = Project::where('name', $this->newProjectName)->first(); if ($type === 'project') {
if ($foundProject) { $foundProject = Project::where('name', $this->newName)->first();
throw new \Exception('Project with the same name already exists.'); if ($foundProject) {
} throw new \Exception('Project with the same name already exists.');
$newProject = Project::create([ }
'name' => $this->newProjectName, $project = Project::create([
'team_id' => currentTeam()->id, 'name' => $this->newName,
'description' => $this->project->description . ' (clone)', 'team_id' => currentTeam()->id,
]); 'description' => $this->project->description . ' (clone)',
if ($this->environment->name !== 'production') { ]);
$newProject->environments()->create([ if ($this->environment->name !== 'production') {
'name' => $this->environment->name, $project->environments()->create([
'name' => $this->environment->name,
]);
}
$environment = $project->environments->where('name', $this->environment->name)->first();
} else {
$foundEnv = $this->project->environments()->where('name', $this->newName)->first();
if ($foundEnv) {
throw new \Exception('Environment with the same name already exists.');
}
$project = $this->project;
$environment = $this->project->environments()->create([
'name' => $this->newName,
]); ]);
} }
$newEnvironment = $newProject->environments->where('name', $this->environment->name)->first();
// Clone Applications
$applications = $this->environment->applications; $applications = $this->environment->applications;
$databases = $this->environment->databases(); $databases = $this->environment->databases();
$services = $this->environment->services; $services = $this->environment->services;
@@ -83,7 +99,7 @@ class CloneMe extends Component
'uuid' => $uuid, 'uuid' => $uuid,
'fqdn' => generateFqdn($this->server, $uuid), 'fqdn' => generateFqdn($this->server, $uuid),
'status' => 'exited', 'status' => 'exited',
'environment_id' => $newEnvironment->id, 'environment_id' => $environment->id,
// This is not correct, but we need to set it to something // This is not correct, but we need to set it to something
'destination_id' => $this->selectedDestination, 'destination_id' => $this->selectedDestination,
]); ]);
@@ -110,7 +126,7 @@ class CloneMe extends Component
'uuid' => $uuid, 'uuid' => $uuid,
'status' => 'exited', 'status' => 'exited',
'started_at' => null, 'started_at' => null,
'environment_id' => $newEnvironment->id, 'environment_id' => $environment->id,
'destination_id' => $this->selectedDestination, 'destination_id' => $this->selectedDestination,
]); ]);
$newDatabase->save(); $newDatabase->save();
@@ -136,7 +152,7 @@ class CloneMe extends Component
$uuid = (string)new Cuid2(7); $uuid = (string)new Cuid2(7);
$newService = $service->replicate()->fill([ $newService = $service->replicate()->fill([
'uuid' => $uuid, 'uuid' => $uuid,
'environment_id' => $newEnvironment->id, 'environment_id' => $environment->id,
'destination_id' => $this->selectedDestination, 'destination_id' => $this->selectedDestination,
]); ]);
$newService->save(); $newService->save();
@@ -153,8 +169,8 @@ class CloneMe extends Component
$newService->parse(); $newService->parse();
} }
return redirect()->route('project.resource.index', [ return redirect()->route('project.resource.index', [
'project_uuid' => $newProject->uuid, 'project_uuid' => $project->uuid,
'environment_name' => $newEnvironment->name, 'environment_name' => $environment->name,
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
return handleError($e, $this); return handleError($e, $this);

View File

@@ -58,19 +58,19 @@ class Heading extends Component
{ {
if ($this->database->type() === 'standalone-postgresql') { if ($this->database->type() === 'standalone-postgresql') {
$activity = StartPostgresql::run($this->database); $activity = StartPostgresql::run($this->database);
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} else if ($this->database->type() === 'standalone-redis') { } else if ($this->database->type() === 'standalone-redis') {
$activity = StartRedis::run($this->database); $activity = StartRedis::run($this->database);
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} else if ($this->database->type() === 'standalone-mongodb') { } else if ($this->database->type() === 'standalone-mongodb') {
$activity = StartMongodb::run($this->database); $activity = StartMongodb::run($this->database);
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} else if ($this->database->type() === 'standalone-mysql') { } else if ($this->database->type() === 'standalone-mysql') {
$activity = StartMysql::run($this->database); $activity = StartMysql::run($this->database);
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} else if ($this->database->type() === 'standalone-mariadb') { } else if ($this->database->type() === 'standalone-mariadb') {
$activity = StartMariadb::run($this->database); $activity = StartMariadb::run($this->database);
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} }
} }
} }

View File

@@ -129,7 +129,7 @@ class Import extends Component
if (!empty($this->importCommands)) { if (!empty($this->importCommands)) {
$activity = remote_process($this->importCommands, $this->server, ignore_errors: true); $activity = remote_process($this->importCommands, $this->server, ignore_errors: true);
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->validated = false; $this->validated = false;

View File

@@ -9,6 +9,7 @@ use App\Models\PrivateKey;
use App\Models\Project; use App\Models\Project;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use App\Models\SwarmDocker; use App\Models\SwarmDocker;
use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url; use Spatie\Url\Url;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@@ -18,7 +19,7 @@ class GithubPrivateRepositoryDeployKey extends Component
public $current_step = 'private_keys'; public $current_step = 'private_keys';
public $parameters; public $parameters;
public $query; public $query;
public $private_keys; public $private_keys =[];
public int $private_key_id; public int $private_key_id;
public int $port = 3000; public int $port = 3000;
@@ -33,6 +34,11 @@ class GithubPrivateRepositoryDeployKey extends Component
public $build_pack = 'nixpacks'; public $build_pack = 'nixpacks';
public bool $show_is_static = true; public bool $show_is_static = true;
private object $repository_url_parsed;
private GithubApp|GitlabApp|string $git_source = 'other';
private ?string $git_host = null;
private string $git_repository;
protected $rules = [ protected $rules = [
'repository_url' => 'required', 'repository_url' => 'required',
'branch' => 'required|string', 'branch' => 'required|string',
@@ -49,10 +55,7 @@ class GithubPrivateRepositoryDeployKey extends Component
'publish_directory' => 'Publish directory', 'publish_directory' => 'Publish directory',
'build_pack' => 'Build pack', 'build_pack' => 'Build pack',
]; ];
private object $repository_url_parsed;
private GithubApp|GitlabApp|string $git_source = 'other';
private ?string $git_host = null;
private string $git_repository;
public function mount() public function mount()
{ {

View File

@@ -29,7 +29,8 @@ class Index extends Component
} }
$this->project = $project; $this->project = $project;
$this->environment = $environment; $this->environment = $environment;
$this->applications = $environment->applications->load(['tags']);
$this->applications = $this->environment->applications->load(['tags']);
$this->applications = $this->applications->map(function ($application) { $this->applications = $this->applications->map(function ($application) {
if (data_get($application, 'environment.project.uuid')) { if (data_get($application, 'environment.project.uuid')) {
$application->hrefLink = route('project.application.configuration', [ $application->hrefLink = route('project.application.configuration', [
@@ -40,8 +41,7 @@ class Index extends Component
} }
return $application; return $application;
}); });
ray($this->applications); $this->postgresqls = $this->environment->postgresqls->load(['tags'])->sortBy('name');
$this->postgresqls = $environment->postgresqls->load(['tags'])->sortBy('name');
$this->postgresqls = $this->postgresqls->map(function ($postgresql) { $this->postgresqls = $this->postgresqls->map(function ($postgresql) {
if (data_get($postgresql, 'environment.project.uuid')) { if (data_get($postgresql, 'environment.project.uuid')) {
$postgresql->hrefLink = route('project.database.configuration', [ $postgresql->hrefLink = route('project.database.configuration', [
@@ -52,7 +52,7 @@ class Index extends Component
} }
return $postgresql; return $postgresql;
}); });
$this->redis = $environment->redis->load(['tags'])->sortBy('name'); $this->redis = $this->environment->redis->load(['tags'])->sortBy('name');
$this->redis = $this->redis->map(function ($redis) { $this->redis = $this->redis->map(function ($redis) {
if (data_get($redis, 'environment.project.uuid')) { if (data_get($redis, 'environment.project.uuid')) {
$redis->hrefLink = route('project.database.configuration', [ $redis->hrefLink = route('project.database.configuration', [
@@ -63,7 +63,7 @@ class Index extends Component
} }
return $redis; return $redis;
}); });
$this->mongodbs = $environment->mongodbs->load(['tags'])->sortBy('name'); $this->mongodbs = $this->environment->mongodbs->load(['tags'])->sortBy('name');
$this->mongodbs = $this->mongodbs->map(function ($mongodb) { $this->mongodbs = $this->mongodbs->map(function ($mongodb) {
if (data_get($mongodb, 'environment.project.uuid')) { if (data_get($mongodb, 'environment.project.uuid')) {
$mongodb->hrefLink = route('project.database.configuration', [ $mongodb->hrefLink = route('project.database.configuration', [
@@ -74,7 +74,7 @@ class Index extends Component
} }
return $mongodb; return $mongodb;
}); });
$this->mysqls = $environment->mysqls->load(['tags'])->sortBy('name'); $this->mysqls = $this->environment->mysqls->load(['tags'])->sortBy('name');
$this->mysqls = $this->mysqls->map(function ($mysql) { $this->mysqls = $this->mysqls->map(function ($mysql) {
if (data_get($mysql, 'environment.project.uuid')) { if (data_get($mysql, 'environment.project.uuid')) {
$mysql->hrefLink = route('project.database.configuration', [ $mysql->hrefLink = route('project.database.configuration', [
@@ -85,7 +85,7 @@ class Index extends Component
} }
return $mysql; return $mysql;
}); });
$this->mariadbs = $environment->mariadbs->load(['tags'])->sortBy('name'); $this->mariadbs = $this->environment->mariadbs->load(['tags'])->sortBy('name');
$this->mariadbs = $this->mariadbs->map(function ($mariadb) { $this->mariadbs = $this->mariadbs->map(function ($mariadb) {
if (data_get($mariadb, 'environment.project.uuid')) { if (data_get($mariadb, 'environment.project.uuid')) {
$mariadb->hrefLink = route('project.database.configuration', [ $mariadb->hrefLink = route('project.database.configuration', [
@@ -96,7 +96,7 @@ class Index extends Component
} }
return $mariadb; return $mariadb;
}); });
$this->services = $environment->services->load(['tags'])->sortBy('name'); $this->services = $this->environment->services->load(['tags'])->sortBy('name');
$this->services = $this->services->map(function ($service) { $this->services = $this->services->map(function ($service) {
if (data_get($service, 'environment.project.uuid')) { if (data_get($service, 'environment.project.uuid')) {
$service->hrefLink = route('project.service.configuration', [ $service->hrefLink = route('project.service.configuration', [

View File

@@ -57,7 +57,7 @@ class Navbar extends Component
} }
$this->service->parse(); $this->service->parse();
$activity = StartService::run($this->service); $activity = StartService::run($this->service);
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} }
public function stop(bool $forceCleanup = false) public function stop(bool $forceCleanup = false)
{ {
@@ -82,6 +82,6 @@ class Navbar extends Component
StopService::run($this->service); StopService::run($this->service);
$this->service->parse(); $this->service->parse();
$activity = StartService::run($this->service); $activity = StartService::run($this->service);
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} }
} }

View File

@@ -5,7 +5,7 @@ namespace App\Livewire\Project\Service;
use App\Models\ServiceApplication; use App\Models\ServiceApplication;
use Livewire\Component; use Livewire\Component;
class Application extends Component class ServiceApplicationView extends Component
{ {
public ServiceApplication $application; public ServiceApplication $application;
public $parameters; public $parameters;
@@ -20,7 +20,7 @@ class Application extends Component
]; ];
public function render() public function render()
{ {
return view('livewire.project.service.application'); return view('livewire.project.service.service-application-view');
} }
public function instantSave() public function instantSave()
{ {

View File

@@ -115,7 +115,7 @@ class ExecuteContainerCommand extends Component
$exec = "docker exec {$this->container} {$cmd}"; $exec = "docker exec {$this->container} {$cmd}";
} }
$activity = remote_process([$exec], $this->server, ignore_errors: true); $activity = remote_process([$exec], $this->server, ignore_errors: true);
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -37,12 +37,13 @@ class Tags extends Component
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function deleteTag($id, $name) public function deleteTag(string $id)
{ {
try { try {
$found_more_tags = Tag::where(['name' => $name, 'team_id' => currentTeam()->id])->first();
$this->resource->tags()->detach($id); $this->resource->tags()->detach($id);
if ($found_more_tags->resources()->get()->count() == 0) {
$found_more_tags = Tag::where(['id' => $id, 'team_id' => currentTeam()->id])->first();
if ($found_more_tags->applications()->count() == 0 && $found_more_tags->services()->count() == 0){
$found_more_tags->delete(); $found_more_tags->delete();
} }
$this->refresh(); $this->refresh();
@@ -53,6 +54,7 @@ class Tags extends Component
public function refresh() public function refresh()
{ {
$this->resource->load(['tags']); $this->resource->load(['tags']);
$this->tags = Tag::ownedByCurrentTeam()->get();
$this->new_tag = null; $this->new_tag = null;
} }
public function submit() public function submit()

View File

@@ -31,7 +31,7 @@ class RunCommand extends Component
$this->validate(); $this->validate();
try { try {
$activity = remote_process([$this->command], Server::where('uuid', $this->server)->first(), ignore_errors: true); $activity = remote_process([$this->command], Server::where('uuid', $this->server)->first(), ignore_errors: true);
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -2,7 +2,6 @@
namespace App\Livewire\Server; namespace App\Livewire\Server;
use App\Actions\Server\InstallDocker;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@@ -14,7 +13,8 @@ class Form extends Component
public ?string $wildcard_domain = null; public ?string $wildcard_domain = null;
public int $cleanup_after_percentage; public int $cleanup_after_percentage;
public bool $dockerInstallationStarted = false; public bool $dockerInstallationStarted = false;
protected $listeners = ['serverRefresh'];
protected $listeners = ['serverInstalled'];
protected $rules = [ protected $rules = [
'server.name' => 'required', 'server.name' => 'required',
@@ -49,9 +49,10 @@ class Form extends Component
$this->wildcard_domain = $this->server->settings->wildcard_domain; $this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage; $this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
} }
public function serverRefresh($install = true) public function serverInstalled()
{ {
$this->validateServer($install); $this->server->refresh();
$this->server->settings->refresh();
} }
public function instantSave() public function instantSave()
{ {
@@ -64,13 +65,6 @@ class Form extends Component
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function installDocker()
{
$this->dispatch('installDocker');
$this->dockerInstallationStarted = true;
$activity = InstallDocker::run($this->server);
$this->dispatch('newMonitorActivity', $activity->id);
}
public function checkLocalhostConnection() public function checkLocalhostConnection()
{ {
$uptime = $this->server->validateConnection(); $uptime = $this->server->validateConnection();
@@ -80,48 +74,13 @@ class Form extends Component
$this->server->settings->is_usable = true; $this->server->settings->is_usable = true;
$this->server->settings->save(); $this->server->settings->save();
} else { } else {
$this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.'); $this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.');
return; return;
} }
} }
public function validateServer($install = true) public function validateServer($install = true)
{ {
try { $this->dispatch('validateServer', $install);
$uptime = $this->server->validateConnection();
if (!$uptime) {
$install && $this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.');
return;
}
$supported_os_type = $this->server->validateOS();
if (!$supported_os_type) {
$install && $this->dispatch('error', 'Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.');
return;
}
$dockerInstalled = $this->server->validateDockerEngine();
if ($dockerInstalled) {
$install && $this->dispatch('success', 'Docker Engine is installed.<br> Checking version.');
} else {
$install && $this->installDocker();
return;
}
$dockerVersion = $this->server->validateDockerEngineVersion();
if ($dockerVersion) {
$install && $this->dispatch('success', 'Docker Engine version is 22+.');
} else {
$install && $this->installDocker();
return;
}
if ($this->server->isSwarm()) {
$swarmInstalled = $this->server->validateDockerSwarm();
if ($swarmInstalled) {
$install && $this->dispatch('success', 'Docker Swarm is initiated.');
}
}
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->dispatch('proxyStatusUpdated');
}
} }
public function submit() public function submit()

View File

@@ -71,7 +71,7 @@ class Deploy extends Component
{ {
try { try {
$activity = StartProxy::run($this->server); $activity = StartProxy::run($this->server);
$this->dispatch('newMonitorActivity', $activity->id, ProxyStatusChanged::class); $this->dispatch('activityMonitor', $activity->id, ProxyStatusChanged::class);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -12,10 +12,8 @@ class Status extends Component
public Server $server; public Server $server;
public bool $polling = false; public bool $polling = false;
public int $numberOfPolls = 0; public int $numberOfPolls = 0;
protected $listeners = ['proxyStatusUpdated', 'startProxyPolling']; protected $listeners = ['proxyStatusUpdated', 'startProxyPolling'];
public function mount() {
}
public function startProxyPolling() public function startProxyPolling()
{ {
$this->checkProxy(); $this->checkProxy();

View File

@@ -11,7 +11,7 @@ class Show extends Component
use AuthorizesRequests; use AuthorizesRequests;
public ?Server $server = null; public ?Server $server = null;
public $parameters = []; public $parameters = [];
protected $listeners = ['proxyStatusUpdated' => '$refresh']; protected $listeners = ['serverInstalled' => '$refresh'];
public function mount() public function mount()
{ {
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();

View File

@@ -39,7 +39,7 @@ class ShowPrivateKey extends Component
if ($uptime) { if ($uptime) {
$this->dispatch('success', 'Server is reachable.'); $this->dispatch('success', 'Server is reachable.');
} else { } else {
$this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/configuration#openssh-server">documentation</a> for further help.'); $this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh#openssh">documentation</a> for further help.');
return; return;
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@@ -0,0 +1,105 @@
<?php
namespace App\Livewire\Server;
use App\Models\Server;
use Livewire\Component;
class ValidateAndInstall extends Component
{
public Server $server;
public int $number_of_tries = 0;
public int $max_tries = 1;
public bool $install = true;
public $uptime = null;
public $supported_os_type = null;
public $docker_installed = null;
public $docker_version = null;
public $error = null;
protected $listeners = ['validateServer' => 'init', 'validateDockerEngine', 'validateServerNow' => 'validateServer'];
public function init(bool $install = true)
{
$this->install = $install;
$this->uptime = null;
$this->supported_os_type = null;
$this->docker_installed = null;
$this->docker_version = null;
$this->error = null;
$this->number_of_tries = 0;
$this->dispatch('validateServerNow');
}
public function validateServer()
{
try {
$this->validateConnection();
$this->validateOS();
$this->validateDockerEngine();
if ($this->server->isSwarm()) {
$swarmInstalled = $this->server->validateDockerSwarm();
if ($swarmInstalled) {
$this->dispatch('success', 'Docker Swarm is initiated.');
}
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function validateConnection()
{
$this->uptime = $this->server->validateConnection();
if (!$this->uptime) {
$this->error = 'Server is not reachable. Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.';
return;
}
}
public function validateOS()
{
$this->supported_os_type = $this->server->validateOS();
if (!$this->supported_os_type) {
$this->error = 'Server OS type is not supported. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
return;
}
}
public function validateDockerEngine()
{
$this->docker_installed = $this->server->validateDockerEngine();
if (!$this->docker_installed) {
if ($this->install) {
ray($this->number_of_tries, $this->max_tries);
if ($this->number_of_tries == $this->max_tries) {
$this->error = 'Docker Engine could not be installed. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
return;
} else {
$activity = $this->server->installDocker();
$this->number_of_tries++;
$this->dispatch('newActivityMonitor', $activity->id, 'validateDockerEngine');
return;
}
} else {
$this->error = 'Docker Engine is not installed. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
return;
}
} else {
$this->validateDockerVersion();
}
}
public function validateDockerVersion()
{
$this->docker_version = $this->server->validateDockerEngineVersion();
if ($this->docker_version) {
$this->dispatch('serverInstalled');
$this->dispatch('success', 'Server validated successfully.');
} else {
$this->error = 'Docker Engine version is not 22+. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
return;
}
}
public function render()
{
return view('livewire.server.validate-and-install');
}
}

View File

@@ -9,15 +9,30 @@ use Livewire\Component;
class Show extends Component class Show extends Component
{ {
public $tags;
public Tag $tag; public Tag $tag;
public $resources; public $applications;
public $services;
public $webhook = null; public $webhook = null;
public $deployments_per_tag_per_server = []; public $deployments_per_tag_per_server = [];
public function mount()
{
$this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name');
$tag = $this->tags->where('name', request()->tag_name)->first();
if (!$tag) {
return redirect()->route('tags.index');
}
$this->webhook = generatTagDeployWebhook($tag->name);
$this->applications = $tag->applications()->get();
$this->services = $tag->services()->get();
$this->tag = $tag;
$this->get_deployments();
}
public function get_deployments() public function get_deployments()
{ {
try { try {
$resource_ids = $this->resources->pluck('id'); $resource_ids = $this->applications->pluck('id');
$this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->whereIn('application_id', $resource_ids)->get([ $this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->whereIn('application_id', $resource_ids)->get([
"id", "id",
"application_id", "application_id",
@@ -35,26 +50,21 @@ class Show extends Component
public function redeploy_all() public function redeploy_all()
{ {
try { try {
$this->resources->each(function ($resource) { $message = collect([]);
$this->applications->each(function ($resource) use ($message) {
$deploy = new Deploy(); $deploy = new Deploy();
$deploy->deploy_resource($resource); $message->push($deploy->deploy_resource($resource));
});
$this->services->each(function ($resource) use ($message) {
$deploy = new Deploy();
$message->push($deploy->deploy_resource($resource));
}); });
$this->dispatch('success', 'Mass deployment started.'); $this->dispatch('success', 'Mass deployment started.');
} catch (\Exception $e) { } catch (\Exception $e) {
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function mount()
{
$tag = Tag::ownedByCurrentTeam()->where('name', request()->tag_name)->first();
if (!$tag) {
return redirect()->route('tags.index');
}
$this->webhook = generatTagDeployWebhook($tag->name);
$this->resources = $tag->resources()->get();
$this->tag = $tag;
$this->get_deployments();
}
public function render() public function render()
{ {
return view('livewire.tags.show'); return view('livewire.tags.show');

View File

@@ -216,6 +216,9 @@ class Application extends BaseModel
{ {
return $this->morphToMany(Tag::class, 'taggable'); return $this->morphToMany(Tag::class, 'taggable');
} }
public function project() {
return data_get($this, 'environment.project');
}
public function team() public function team()
{ {
return data_get($this, 'environment.project.team'); return data_get($this, 'environment.project.team');

View File

@@ -13,6 +13,8 @@ class Environment extends Model
return $this->applications()->count() == 0 && return $this->applications()->count() == 0 &&
$this->redis()->count() == 0 && $this->redis()->count() == 0 &&
$this->postgresqls()->count() == 0 && $this->postgresqls()->count() == 0 &&
$this->mysqls()->count() == 0 &&
$this->mariadbs()->count() == 0 &&
$this->mongodbs()->count() == 0 && $this->mongodbs()->count() == 0 &&
$this->services()->count() == 0; $this->services()->count() == 0;
} }

View File

@@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use App\Actions\Server\InstallDocker;
use App\Enums\ProxyStatus; use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes; use App\Enums\ProxyTypes;
use App\Notifications\Server\Revived; use App\Notifications\Server\Revived;
@@ -411,6 +412,11 @@ class Server extends BaseModel
return true; return true;
} }
public function installDocker()
{
$activity = InstallDocker::run($this);
return $activity;
}
public function validateDockerEngine($throwError = false) public function validateDockerEngine($throwError = false)
{ {
$dockerBinary = instant_remote_process(["command -v docker"], $this, false); $dockerBinary = instant_remote_process(["command -v docker"], $this, false);

View File

@@ -16,6 +16,10 @@ class Service extends BaseModel
{ {
return 'service'; return 'service';
} }
public function project()
{
return data_get($this, 'environment.project');
}
public function team() public function team()
{ {
return data_get($this, 'environment.project.team'); return data_get($this, 'environment.project.team');

View File

@@ -10,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMariadb extends BaseModel class StandaloneMariadb extends BaseModel
{ {
use HasFactory,SoftDeletes; use HasFactory, SoftDeletes;
protected $guarded = []; protected $guarded = [];
protected $casts = [ protected $casts = [

View File

@@ -24,9 +24,8 @@ class Tag extends BaseModel
{ {
return $this->morphedByMany(Application::class, 'taggable'); return $this->morphedByMany(Application::class, 'taggable');
} }
public function services()
public function resources() { {
return $this->applications(); return $this->morphedByMany(Service::class, 'taggable');
} }
} }

View File

@@ -1,23 +1,35 @@
<?php <?php
use App\Enums\ApplicationDeploymentStatus;
use App\Jobs\ApplicationDeploymentJob; use App\Jobs\ApplicationDeploymentJob;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker;
use Spatie\Url\Url; use Spatie\Url\Url;
function queue_application_deployment(Application $application, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null) function queue_application_deployment(Application $application, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, Server $server = null, StandaloneDocker $destination = null)
{ {
$application_id = $application->id; $application_id = $application->id;
$deployment_link = Url::fromString($application->link() . "/deployment/{$deployment_uuid}"); $deployment_link = Url::fromString($application->link() . "/deployment/{$deployment_uuid}");
$deployment_url = $deployment_link->getPath(); $deployment_url = $deployment_link->getPath();
$server_id = $application->destination->server->id; $server_id = $application->destination->server->id;
$server_name = $application->destination->server->name; $server_name = $application->destination->server->name;
$destination_id = $application->destination->id;
if ($server) {
$server_id = $server->id;
$server_name = $server->name;
}
if ($destination) {
$destination_id = $destination->id;
}
$deployment = ApplicationDeploymentQueue::create([ $deployment = ApplicationDeploymentQueue::create([
'application_id' => $application_id, 'application_id' => $application_id,
'application_name' => $application->name, 'application_name' => $application->name,
'server_id' => $server_id, 'server_id' => $server_id,
'server_name' => $server_name, 'server_name' => $server_name,
'destination_id' => $destination_id,
'deployment_uuid' => $deployment_uuid, 'deployment_uuid' => $deployment_uuid,
'deployment_url' => $deployment_url, 'deployment_url' => $deployment_url,
'pull_request_id' => $pull_request_id, 'pull_request_id' => $pull_request_id,
@@ -28,18 +40,39 @@ function queue_application_deployment(Application $application, string $deployme
'git_type' => $git_type 'git_type' => $git_type
]); ]);
if (next_queuable($server_id, $application_id)) { if ($no_questions_asked) {
$deployment->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id,
));
} else if (next_queuable($server_id, $application_id)) {
$deployment->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
dispatch(new ApplicationDeploymentJob( dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id, application_deployment_queue_id: $deployment->id,
)); ));
} }
} }
function force_start_deployment(ApplicationDeploymentQueue $deployment)
{
$deployment->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id,
));
}
function queue_next_deployment(Application $application) function queue_next_deployment(Application $application)
{ {
$server_id = $application->destination->server_id; $server_id = $application->destination->server_id;
$next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', 'queued')->get()->sortBy('created_at')->first(); $next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', 'queued')->get()->sortBy('created_at')->first();
if ($next_found) { if ($next_found) {
$next_found->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
dispatch(new ApplicationDeploymentJob( dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $next_found->id, application_deployment_queue_id: $next_found->id,
)); ));

View File

@@ -104,7 +104,7 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
ray($error); ray($error);
if ($error instanceof TooManyRequestsException) { if ($error instanceof TooManyRequestsException) {
if (isset($livewire)) { if (isset($livewire)) {
return $livewire->dispatch('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds."); return $livewire->dispatch('error', "Too many requests.","Please try again in {$error->secondsUntilAvailable} seconds.");
} }
return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds."; return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.";
} }
@@ -298,10 +298,8 @@ function validate_cron_expression($expression_to_validate): bool
function send_internal_notification(string $message): void function send_internal_notification(string $message): void
{ {
try { try {
$baseUrl = config('app.name');
$team = Team::find(0); $team = Team::find(0);
$team?->notify(new GeneralNotification("👀 {$baseUrl}: " . $message)); $team?->notify(new GeneralNotification($message));
ray("👀 {$baseUrl}: " . $message);
} catch (\Throwable $e) { } catch (\Throwable $e) {
ray($e->getMessage()); ray($e->getMessage());
} }

View File

@@ -3,6 +3,7 @@
return [ return [
'docs' => 'https://coolify.io/docs/', 'docs' => 'https://coolify.io/docs/',
'contact' => 'https://coolify.io/docs/contact', 'contact' => 'https://coolify.io/docs/contact',
'feedback_discord_webhook' => env('FEEDBACK_DISCORD_WEBHOOK'),
'self_hosted' => env('SELF_HOSTED', true), 'self_hosted' => env('SELF_HOSTED', true),
'waitlist' => env('WAITLIST', false), 'waitlist' => env('WAITLIST', false),
'license_url' => 'https://licenses.coollabs.io', 'license_url' => 'https://licenses.coollabs.io',

View File

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

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.205'; return '4.0.0-beta.211';

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('application_deployment_queues', function (Blueprint $table) {
$table->string('destination_id')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_deployment_queues', function (Blueprint $table) {
$table->dropColumn('destination_id');
});
}
};

View File

@@ -46,6 +46,7 @@ services:
- PUSHER_APP_SECRET - PUSHER_APP_SECRET
- AUTOUPDATE - AUTOUPDATE
- SELF_HOSTED - SELF_HOSTED
- FEEDBACK_DISCORD_WEBHOOK
- WAITLIST - WAITLIST
- SUBSCRIPTION_PROVIDER - SUBSCRIPTION_PROVIDER
- STRIPE_API_KEY - STRIPE_API_KEY

View File

@@ -76,7 +76,7 @@ a {
} }
.box { .box {
@apply flex p-2 transition-colors cursor-pointer min-h-[4rem] bg-coolgray-100 hover:bg-coollabs-100 hover:text-white hover:no-underline min-w-[24rem]; @apply flex p-2 transition-colors cursor-pointer min-h-[4rem] bg-coolgray-100 hover:bg-coollabs-100 hover:text-white hover:no-underline;
} }
.box-without-bg { .box-without-bg {

View File

@@ -26,11 +26,10 @@
</li> </li>
<li title="Send us feedback or get help!" class="fixed top-0 right-0 p-2 px-4 pt-4 mt-auto text-xs"> <li title="Send us feedback or get help!" class="fixed top-0 right-0 p-2 px-4 pt-4 mt-auto text-xs">
<div class="justify-center" wire:click="help" onclick="help.showModal()"> <div class="justify-center" wire:click="help" onclick="help.showModal()">
<svg class="w-5 h-5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="icon" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" <path fill="currentColor"
d="M22 5.5H9c-1.1 0-2 .9-2 2v9a2 2 0 0 0 2 2h13c1.11 0 2-.89 2-2v-9a2 2 0 0 0-2-2m0 11H9V9.17l6.5 3.33L22 9.17v7.33m-6.5-5.69L9 7.5h13l-6.5 3.31M5 16.5c0 .17.03.33.05.5H1c-.552 0-1-.45-1-1s.448-1 1-1h4v1.5M3 7h2.05c-.02.17-.05.33-.05.5V9H3c-.55 0-1-.45-1-1s.45-1 1-1m-2 5c0-.55.45-1 1-1h3v2H2c-.55 0-1-.45-1-1Z" /> d="M144 180a16 16 0 1 1-16-16a16 16 0 0 1 16 16m92-52A108 108 0 1 1 128 20a108.12 108.12 0 0 1 108 108m-24 0a84 84 0 1 0-84 84a84.09 84.09 0 0 0 84-84m-84-64c-24.26 0-44 17.94-44 40v4a12 12 0 0 0 24 0v-4c0-8.82 9-16 20-16s20 7.18 20 16s-9 16-20 16a12 12 0 0 0-12 12v8a12 12 0 0 0 23.73 2.56C158.31 137.88 172 122.37 172 104c0-22.06-19.74-40-44-40" />
</svg> </svg>
Feedback
</div> </div>
</li> </li>
<li class="pb-6" title="Logout"> <li class="pb-6" title="Logout">

View File

@@ -142,13 +142,12 @@
</li> </li>
@endif @endif
@if (isSubscriptionActive() || isDev()) @if (isSubscriptionActive() || isDev())
<li title="Send us feedback or get help!" class="fixed top-0 right-0 p-2 px-4 pt-4 mt-auto text-xs"> <li title="Send us feedback or get help!" class="hover:bg-transparent">
<div class="justify-center" wire:click="help" onclick="help.showModal()"> <div class="justify-center" wire:click="help" onclick="help.showModal()">
<svg class="w-5 h-5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="icon" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" <path fill="currentColor"
d="M22 5.5H9c-1.1 0-2 .9-2 2v9a2 2 0 0 0 2 2h13c1.11 0 2-.89 2-2v-9a2 2 0 0 0-2-2m0 11H9V9.17l6.5 3.33L22 9.17v7.33m-6.5-5.69L9 7.5h13l-6.5 3.31M5 16.5c0 .17.03.33.05.5H1c-.552 0-1-.45-1-1s.448-1 1-1h4v1.5M3 7h2.05c-.02.17-.05.33-.05.5V9H3c-.55 0-1-.45-1-1s.45-1 1-1m-2 5c0-.55.45-1 1-1h3v2H2c-.55 0-1-.45-1-1Z" /> d="M144 180a16 16 0 1 1-16-16a16 16 0 0 1 16 16m92-52A108 108 0 1 1 128 20a108.12 108.12 0 0 1 108 108m-24 0a84 84 0 1 0-84 84a84.09 84.09 0 0 0 84-84m-84-64c-24.26 0-44 17.94-44 40v4a12 12 0 0 0 24 0v-4c0-8.82 9-16 20-16s20 7.18 20 16s-9 16-20 16a12 12 0 0 0-12 12v8a12 12 0 0 0 23.73 2.56C158.31 137.88 172 122.37 172 104c0-22.06-19.74-40-44-40" />
</svg> </svg>
Feedback
</div> </div>
</li> </li>
@endif @endif

View File

@@ -4,15 +4,22 @@
'isErrorButton' => false, 'isErrorButton' => false,
'disabled' => false, 'disabled' => false,
'action' => 'delete', 'action' => 'delete',
'content' => null,
]) ])
<div x-data="{ modalOpen: false }" @keydown.escape.window="modalOpen = false" :class="{ 'z-40': modalOpen }" <div x-data="{ modalOpen: false }" @keydown.escape.window="modalOpen = false" :class="{ 'z-40': modalOpen }"
class="relative w-auto h-auto"> class="relative w-auto h-auto">
@if ($disabled) @if ($content)
<x-forms.button isError disabled>{{ $buttonTitle }}</x-forms.button> <div @click="modalOpen=true">
@elseif ($isErrorButton) {{ $content }}
<x-forms.button isError @click="modalOpen=true">{{ $buttonTitle }}</x-forms.button> </div>
@else @else
<x-forms.button @click="modalOpen=true">{{ $buttonTitle }}</x-forms.button> @if ($disabled)
<x-forms.button isError disabled>{{ $buttonTitle }}</x-forms.button>
@elseif ($isErrorButton)
<x-forms.button isError @click="modalOpen=true">{{ $buttonTitle }}</x-forms.button>
@else
<x-forms.button @click="modalOpen=true">{{ $buttonTitle }}</x-forms.button>
@endif
@endif @endif
<template x-teleport="body"> <template x-teleport="body">
<div x-show="modalOpen" class="fixed top-0 left-0 z-[99] flex items-center justify-center w-screen h-screen" <div x-show="modalOpen" class="fixed top-0 left-0 z-[99] flex items-center justify-center w-screen h-screen"
@@ -30,13 +37,13 @@
class="relative w-full py-6 border rounded shadow-lg bg-coolgray-100 px-7 border-coolgray-300 sm:max-w-lg"> class="relative w-full py-6 border rounded shadow-lg bg-coolgray-100 px-7 border-coolgray-300 sm:max-w-lg">
<div class="flex items-center justify-between pb-3"> <div class="flex items-center justify-between pb-3">
<h3 class="text-2xl font-bold">{{ $title }}</h3> <h3 class="text-2xl font-bold">{{ $title }}</h3>
<button @click="modalOpen=false" {{-- <button @click="modalOpen=false"
class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 text-white rounded-full hover:bg-coolgray-300"> class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 text-white rounded-full hover:bg-coolgray-300">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" <svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor"> stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button> --}}
</div> </div>
<div class="relative w-auto pb-8"> <div class="relative w-auto pb-8">
{{ $slot }} {{ $slot }}
@@ -48,14 +55,13 @@
<div class="flex-1"></div> <div class="flex-1"></div>
@if ($isErrorButton) @if ($isErrorButton)
<x-forms.button @click="modalOpen=false" class="w-24" isError type="button" <x-forms.button @click="modalOpen=false" class="w-24" isError type="button"
wire:click.prevent='{{ $action }}'>Continue wire:click.prevent="{{ $action }}">Continue
</x-forms.button> </x-forms.button>
@else @else
<x-forms.button @click="modalOpen=false" class="w-24" isHighlighted type="button" <x-forms.button @click="modalOpen=false" class="w-24" isHighlighted type="button"
wire:click.prevent='{{ $action }}'>Continue wire:click.prevent="{{ $action }}">Continue
</x-forms.button> </x-forms.button>
@endif @endif
</div> </div>
</div> </div>
</div> </div>

View File

@@ -258,7 +258,8 @@
<div class="flex items-start gap-4 text-xl tracking-tight">Need official support for <div class="flex items-start gap-4 text-xl tracking-tight">Need official support for
your self-hosted instance? your self-hosted instance?
<x-forms.button> <x-forms.button>
<a class="font-bold text-white hover:no-underline" href="{{ config('coolify.contact') }}">Contact <a class="font-bold text-white hover:no-underline"
href="{{ config('coolify.contact') }}">Contact
Us</a> Us</a>
</x-forms.button> </x-forms.button>
</div> </div>

View File

@@ -1,31 +1,37 @@
@props(['closeWithX' => 'false', 'fullScreen' => 'false'])
<div x-data="{ <div x-data="{
slideOverOpen: false slideOverOpen: false
}" class="relative w-auto h-auto"> }" class="relative w-auto h-auto">
{{ $slot }} {{ $slot }}
<template x-teleport="body"> <template x-teleport="body">
<div x-show="slideOverOpen" @keydown.window.escape="slideOverOpen=false" class="relative z-[99]"> <div x-show="slideOverOpen" @if (!$closeWithX) @keydown.window.escape="slideOverOpen=false" @endif
<div x-show="slideOverOpen" @click="slideOverOpen = false" class="fixed inset-0 bg-black bg-opacity-60"></div> class="relative z-[99]">
<div x-show="slideOverOpen" @if (!$closeWithX) @click="slideOverOpen = false" @endif
class="fixed inset-0 bg-black bg-opacity-60"></div>
<div class="fixed inset-0 overflow-hidden"> <div class="fixed inset-0 overflow-hidden">
<div class="absolute inset-0 overflow-hidden"> <div class="absolute inset-0 overflow-hidden">
<div class="fixed inset-y-0 right-0 flex max-w-full pl-10"> <div class="fixed inset-y-0 right-0 flex max-w-full pl-10">
<div x-show="slideOverOpen" @click.away="slideOverOpen = false" <div x-show="slideOverOpen"
@if (!$closeWithX) @click.away="slideOverOpen = false" @endif
x-transition:enter="transform transition ease-in-out duration-100 sm:duration-300" x-transition:enter="transform transition ease-in-out duration-100 sm:duration-300"
x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0" x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0"
x-transition:leave="transform transition ease-in-out duration-100 sm:duration-300" x-transition:leave="transform transition ease-in-out duration-100 sm:duration-300"
x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full" x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full"
class="w-screen max-w-md"> @class([
'max-w-md w-screen' => !$fullScreen,
'max-w-7xl w-screen' => $fullScreen,
])>
<div <div
class="flex flex-col h-full py-6 overflow-hidden border-l shadow-lg bg-base-100 border-neutral-800"> class="flex flex-col h-full py-6 overflow-hidden border-l shadow-lg bg-base-100 border-neutral-800">
<div class="px-4 pb-10 sm:px-5"> <div class="px-4 pb-4 sm:px-5">
<div class="flex items-start justify-between pb-1"> <div class="flex items-start justify-between pb-1">
<h2 class="text-2xl leading-6" id="slide-over-title"> <h2 class="text-3xl leading-6" id="slide-over-title">
{{ $title }}</h2> {{ $title }}</h2>
<div class="flex items-center h-auto ml-3"> <div class="flex items-center h-auto ml-3">
<button class="icon" @click="slideOverOpen=false" <button class="icon" @click="slideOverOpen=false"
class="absolute top-0 right-0 z-30 flex items-center justify-center px-3 py-2 mt-4 mr-2 space-x-1 text-xs font-normal border-none rounded"> class="absolute top-0 right-0 z-30 flex items-center justify-center px-3 py-2 mt-4 mr-2 space-x-1 text-xs font-normal border-none rounded">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" fill="none" <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" fill="none"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
>
<path stroke-linecap="round" stroke-linejoin="round" <path stroke-linecap="round" stroke-linejoin="round"
d="M6 18L18 6M6 6l12 12"></path> d="M6 18L18 6M6 6l12 12"></path>
</svg> </svg>

View File

@@ -170,6 +170,7 @@
} }
}) })
window.Livewire.on('installDocker', () => { window.Livewire.on('installDocker', () => {
console.log('Installing Docker...');
installDocker.showModal(); installDocker.showModal();
}) })
}); });

View File

@@ -1,23 +1,12 @@
@extends('layouts.base') @extends('layouts.base')
@section('body') @section('body')
<x-modal noSubmit modalId="installDocker">
<x-slot:modalBody>
<livewire:activity-monitor header="Docker Installation Logs" />
</x-slot:modalBody>
<x-slot:modalSubmit>
<x-forms.button onclick="installDocker.close()" type="submit">
Close
</x-forms.button>
</x-slot:modalSubmit>
</x-modal>
@if (isSubscriptionActive() || isDev()) @if (isSubscriptionActive() || isDev())
<div title="Send us feedback or get help!" class="fixed top-0 right-0 p-2 px-4 pt-4 mt-auto text-xs"> <div title="Send us feedback or get help!" class="fixed top-0 right-0 p-2 px-4 pt-4 mt-auto text-xs">
<button class="flex items-center justify-center gap-2" wire:click="help" onclick="help.showModal()"> <button class="flex items-center justify-center gap-2" wire:click="help" onclick="help.showModal()">
<svg class="w-5 h-5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="icon" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" <path fill="currentColor"
d="M22 5.5H9c-1.1 0-2 .9-2 2v9a2 2 0 0 0 2 2h13c1.11 0 2-.89 2-2v-9a2 2 0 0 0-2-2m0 11H9V9.17l6.5 3.33L22 9.17v7.33m-6.5-5.69L9 7.5h13l-6.5 3.31M5 16.5c0 .17.03.33.05.5H1c-.552 0-1-.45-1-1s.448-1 1-1h4v1.5M3 7h2.05c-.02.17-.05.33-.05.5V9H3c-.55 0-1-.45-1-1s.45-1 1-1m-2 5c0-.55.45-1 1-1h3v2H2c-.55 0-1-.45-1-1Z" /> d="M144 180a16 16 0 1 1-16-16a16 16 0 0 1 16 16m92-52A108 108 0 1 1 128 20a108.12 108.12 0 0 1 108 108m-24 0a84 84 0 1 0-84 84a84.09 84.09 0 0 0 84-84m-84-64c-24.26 0-44 17.94-44 40v4a12 12 0 0 0 24 0v-4c0-8.82 9-16 20-16s20 7.18 20 16s-9 16-20 16a12 12 0 0 0-12 12v8a12 12 0 0 0 23.73 2.56C158.31 137.88 172 122.37 172 104c0-22.06-19.74-40-44-40" />
</svg> </svg>
Feedback
</button> </button>
</div> </div>
@endif @endif

View File

@@ -12,7 +12,7 @@
<x-navbar-subscription /> <x-navbar-subscription />
@endif @endif
<main class="main max-w-screen-2xl"> <main class="mx-auto main max-w-screen-2xl">
{{ $slot }} {{ $slot }}
</main> </main>
@endsection @endsection

View File

@@ -5,7 +5,8 @@
<h1 class="text-5xl font-bold">Welcome to Coolify</h1> <h1 class="text-5xl font-bold">Welcome to Coolify</h1>
<p class="py-6 text-xl text-center">Let me help you to set the basics.</p> <p class="py-6 text-xl text-center">Let me help you to set the basics.</p>
<div class="flex justify-center "> <div class="flex justify-center ">
<x-forms.button class="justify-center box" wire:click="$set('currentState','explanation')">Get Started <x-forms.button class="justify-center w-64 box" wire:click="$set('currentState','explanation')">Get
Started
</x-forms.button> </x-forms.button>
</div> </div>
@endif @endif
@@ -31,7 +32,7 @@
Telegram, Email, etc.) when something goes wrong, or an action needed from your side.</p> Telegram, Email, etc.) when something goes wrong, or an action needed from your side.</p>
</x-slot:explanation> </x-slot:explanation>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center box" wire:click="explanation">Next <x-forms.button class="justify-center w-64 box" wire:click="explanation">Next
</x-forms.button> </x-forms.button>
</x-slot:actions> </x-slot:actions>
</x-boarding-step> </x-boarding-step>
@@ -43,11 +44,11 @@
or on a <x-highlighted text="Remote Server" />? or on a <x-highlighted text="Remote Server" />?
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center box" wire:target="setServerType('localhost')" <x-forms.button class="justify-center w-64 box" wire:target="setServerType('localhost')"
wire:click="setServerType('localhost')">Localhost wire:click="setServerType('localhost')">Localhost
</x-forms.button> </x-forms.button>
<x-forms.button class="justify-center box" wire:target="setServerType('remote')" <x-forms.button class="justify-center w-64 box " wire:target="setServerType('remote')"
wire:click="setServerType('remote')">Remote Server wire:click="setServerType('remote')">Remote Server
</x-forms.button> </x-forms.button>
@if (!$serverReachable) @if (!$serverReachable)
@@ -57,9 +58,10 @@
'root' or skip the boarding process and add a new private key manually to Coolify and to the 'root' or skip the boarding process and add a new private key manually to Coolify and to the
server. server.
<br /> <br />
Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help. Check this <a target="_blank" class="underline"
href="https://coolify.io/docs/server/openssh">documentation</a> for further help.
<x-forms.input readonly id="serverPublicKey"></x-forms.input> <x-forms.input readonly id="serverPublicKey"></x-forms.input>
<x-forms.button class="box" wire:target="setServerType('localhost')" <x-forms.button class="w-64 box" wire:target="setServerType('localhost')"
wire:click="setServerType('localhost')">Check again wire:click="setServerType('localhost')">Check again
</x-forms.button> </x-forms.button>
@endif @endif
@@ -83,10 +85,10 @@
Do you have your own SSH Private Key? Do you have your own SSH Private Key?
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center box" wire:target="setPrivateKey('own')" <x-forms.button class="justify-center w-64 box" wire:target="setPrivateKey('own')"
wire:click="setPrivateKey('own')">Yes wire:click="setPrivateKey('own')">Yes
</x-forms.button> </x-forms.button>
<x-forms.button class="justify-center box" wire:target="setPrivateKey('create')" <x-forms.button class="justify-center w-64 box" wire:target="setPrivateKey('create')"
wire:click="setPrivateKey('create')">No (create one for me) wire:click="setPrivateKey('create')">No (create one for me)
</x-forms.button> </x-forms.button>
@if (count($privateKeys) > 0) @if (count($privateKeys) > 0)
@@ -119,7 +121,7 @@
There are already servers available for your Team. Do you want to use one of them? There are already servers available for your Team. Do you want to use one of them?
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center box" wire:click="createNewServer">No (create one for me) <x-forms.button class="justify-center w-64 box" wire:click="createNewServer">No (create one for me)
</x-forms.button> </x-forms.button>
<div> <div>
<form wire:submit='selectExistingServer' class="flex flex-col w-full gap-4 lg:w-96"> <form wire:submit='selectExistingServer' class="flex flex-col w-full gap-4 lg:w-96">
@@ -139,7 +141,7 @@
'root' or skip the boarding process and add a new private key manually to Coolify and to the 'root' or skip the boarding process and add a new private key manually to Coolify and to the
server. server.
<x-forms.input readonly id="serverPublicKey"></x-forms.input> <x-forms.input readonly id="serverPublicKey"></x-forms.input>
<x-forms.button class="box" wire:target="validateServer" wire:click="validateServer">Check <x-forms.button class="w-64 box" wire:target="validateServer" wire:click="validateServer">Check
again again
</x-forms.button> </x-forms.button>
@endif @endif
@@ -231,12 +233,16 @@
Could not find Docker Engine on your server. Do you want me to install it for you? Could not find Docker Engine on your server. Do you want me to install it for you?
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center box" wire:click="installDocker"> <x-slide-over closeWithX fullScreen>
Let's do it!</x-forms.button> <x-slot:title>Configuring Server</x-slot:title>
@if ($dockerInstallationStarted) <x-slot:content>
<x-forms.button class="justify-center box" wire:click="dockerInstalledOrSkipped"> <livewire:server.validate-and-install :server="$this->createdServer" />
Validate Server & Continue</x-forms.button> </x-slot:content>
@endif <x-forms.button @click="slideOverOpen=true" class="font-bold box w-96"
wire:click.prevent='installServer' isHighlighted>
Let's do it!
</x-forms.button>
</x-slide-over>
</x-slot:actions> </x-slot:actions>
<x-slot:explanation> <x-slot:explanation>
<p>This will install the latest Docker Engine on your server, configure a few things to be able <p>This will install the latest Docker Engine on your server, configure a few things to be able
@@ -246,7 +252,6 @@
documentation</a>.</p> documentation</a>.</p>
</x-slot:explanation> </x-slot:explanation>
</x-boarding-step> </x-boarding-step>
@endif @endif
</div> </div>
<div> <div>
@@ -289,7 +294,7 @@
@endif @endif
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center box" wire:click="createNewProject">Let's create a new <x-forms.button class="justify-center w-64 box" wire:click="createNewProject">Let's create a new
one!</x-forms.button> one!</x-forms.button>
<div> <div>
@if (count($projects) > 0) @if (count($projects) > 0)
@@ -322,7 +327,7 @@
I will redirect you to the new resource page, where you can create your first resource. I will redirect you to the new resource page, where you can create your first resource.
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<div class="items-center justify-center box" wire:click="showNewResource">Let's do <div class="items-center justify-center w-64 box" wire:click="showNewResource">Let's do
it!</div> it!</div>
</x-slot:actions> </x-slot:actions>
<x-slot:explanation> <x-slot:explanation>

View File

@@ -6,6 +6,6 @@
<x-forms.textarea rows="10" id="description" label="Description" <x-forms.textarea rows="10" id="description" label="Description"
placeholder="Please provide as much information as possible."></x-forms.textarea> placeholder="Please provide as much information as possible."></x-forms.textarea>
<div></div> <div></div>
<x-forms.button class="w-full mt-4" type="submit" onclick="help.close()">Send Email</x-forms.button> <x-forms.button class="w-full mt-4" type="submit" onclick="help.close()">Send</x-forms.button>
</form> </form>
</div> </div>

View File

@@ -0,0 +1,18 @@
@php use App\Actions\CoolifyTask\RunRemoteProcess; @endphp
<div>
@if ($this->activity)
@if (isset($header))
<div class="flex gap-2 pb-2">
{{ $header }}
@if ($isPollingActive)
<x-loading />
@endif
</div>
@endif
<div
class="scrollbar flex flex-col-reverse w-full overflow-y-auto border border-solid rounded border-coolgray-300 max-h-[32rem] p-4 pt-6 text-xs text-white">
<pre class="font-mono whitespace-pre-wrap" @if ($isPollingActive) wire:poll.1000ms="polling" @endif>{{ RunRemoteProcess::decodeOutput($this->activity) }}</pre>
</div>
@endif
</div>

View File

@@ -5,8 +5,12 @@
@else @else
<x-forms.button wire:click.prevent="show_debug">Show Debug Logs</x-forms.button> <x-forms.button wire:click.prevent="show_debug">Show Debug Logs</x-forms.button>
@endif @endif
@if (data_get($application_deployment_queue, 'status') === 'queued')
<x-forms.button wire:click.prevent="force_start">Force Start</x-forms.button>
@endif
@if (data_get($application_deployment_queue, 'status') === 'in_progress' || @if (data_get($application_deployment_queue, 'status') === 'in_progress' ||
data_get($application_deployment_queue, 'status') === 'queued') data_get($application_deployment_queue, 'status') === 'queued')
<x-forms.button isError wire:click.prevent="cancel">Cancel Deployment</x-forms.button> <x-forms.button isError wire:click.prevent="cancel">Cancel</x-forms.button>
@endif @endif
</div> </div>

View File

@@ -106,18 +106,6 @@
</svg> </svg>
Deploy Deploy
</button> </button>
{{-- @if (isDev())
<button wire:click='deployNew'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy (new)
</button>
@endif --}}
@endif @endif
@endif @endif
</div> </div>

View File

@@ -1,19 +1,17 @@
<form wire:submit='clone'> <form>
<div class="flex flex-col"> <div class="flex flex-col">
<h1>Clone</h1> <h1>Clone</h1>
<div class="subtitle ">Quickly clone all resources to a new project</div> <div class="subtitle ">Quickly clone all resources to a new project or environment</div>
</div>
<div class="flex items-end gap-2">
<x-forms.input required id="newProjectName" label="New Project Name" />
<x-forms.button isHighlighted type="submit">Clone</x-forms.button>
</div> </div>
<x-forms.input required id="newName" label="New Name" />
<x-forms.button isHighlighted wire:click="clone('project')" class="mt-4">Clone to a new Project</x-forms.button>
<x-forms.button isHighlighted wire:click="clone('environment')" class="mt-4">Clone to a new Environment</x-forms.button>
<h3 class="pt-4 pb-2">Servers</h3> <h3 class="pt-4 pb-2">Servers</h3>
<div>Choose the server and network to clone the resources to.</div> <div>Choose the server and network to clone the resources to.</div>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
@foreach ($servers->sortBy('id') as $server) @foreach ($servers->sortBy('id') as $server)
<div class="p-4"> <div class="p-4">
<h4>{{ $server->name }}</h4> <h4>{{ $server->name }}</h4>
<h5>{{ $server->description }}</h5>
<div class="pt-4 pb-2">Docker Networks</div> <div class="pt-4 pb-2">Docker Networks</div>
<div class="grid grid-cols-1 gap-2 pb-4 lg:grid-cols-4"> <div class="grid grid-cols-1 gap-2 pb-4 lg:grid-cols-4">
@foreach ($server->destinations() as $destination) @foreach ($server->destinations() as $destination)
@@ -30,9 +28,9 @@
<h3 class="pt-4 pb-2">Resources</h3> <h3 class="pt-4 pb-2">Resources</h3>
<div>These will be cloned to the new project</div> <div>These will be cloned to the new project</div>
<div class="grid grid-cols-1 gap-2 p-4 "> <div class="grid grid-cols-1 gap-2 pt-4 opacity-95 lg:grid-cols-2 xl:grid-cols-3">
@foreach ($environment->applications->sortBy('name') as $application) @foreach ($environment->applications->sortBy('name') as $application)
<div> <div class="cursor-default box-without-bg bg-coolgray-100 group">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="font-bold text-white">{{ $application->name }}</div> <div class="font-bold text-white">{{ $application->name }}</div>
<div class="description">{{ $application->description }}</div> <div class="description">{{ $application->description }}</div>
@@ -40,7 +38,7 @@
</div> </div>
@endforeach @endforeach
@foreach ($environment->databases()->sortBy('name') as $database) @foreach ($environment->databases()->sortBy('name') as $database)
<div> <div class="cursor-default box-without-bg bg-coolgray-100 group">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="font-bold text-white">{{ $database->name }}</div> <div class="font-bold text-white">{{ $database->name }}</div>
<div class="description">{{ $database->description }}</div> <div class="description">{{ $database->description }}</div>
@@ -48,7 +46,7 @@
</div> </div>
@endforeach @endforeach
@foreach ($environment->services->sortBy('name') as $service) @foreach ($environment->services->sortBy('name') as $service)
<div> <div class="cursor-default box-without-bg bg-coolgray-100 group">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="font-bold text-white">{{ $service->name }}</div> <div class="font-bold text-white">{{ $service->name }}</div>
<div class="description">{{ $service->description }}</div> <div class="description">{{ $service->description }}</div>

View File

@@ -8,7 +8,7 @@
<li class="step">Select a Repository, Branch & Save</li> <li class="step">Select a Repository, Branch & Save</li>
</ul> </ul>
<div class="flex flex-col justify-center gap-2 text-left xl:flex-row"> <div class="flex flex-col justify-center gap-2 text-left xl:flex-row">
@foreach ($private_keys as $key) @forelse ($private_keys as $key)
@if ($private_key_id == $key->id) @if ($private_key_id == $key->id)
<div class="gap-2 py-4 cursor-pointer group hover:bg-coollabs bg-coolgray-200" <div class="gap-2 py-4 cursor-pointer group hover:bg-coollabs bg-coolgray-200"
wire:click.defer="setPrivateKey('{{ $key->id }}')" wire:key="{{ $key->id }}"> wire:click.defer="setPrivateKey('{{ $key->id }}')" wire:key="{{ $key->id }}">
@@ -32,7 +32,16 @@
</div> </div>
</div> </div>
@endif @endif
@endforeach @empty
<div class="flex flex-col items-center justify-center gap-2">
<div class="text-neutral-500">
No private keys found.
</div>
<a href="{{ route('security.private-key.index') }}">
<x-forms.button>Create a new private key</x-forms.button>
</a>
</div>
@endforelse
</div> </div>
@endif @endif
@if ($current_step === 'repository') @if ($current_step === 'repository')

View File

@@ -46,7 +46,7 @@
@else @else
<div x-data="searchComponent()"> <div x-data="searchComponent()">
<x-forms.input placeholder="Search for name, fqdn..." class="w-full" x-model="search" /> <x-forms.input placeholder="Search for name, fqdn..." class="w-full" x-model="search" />
<div class="grid gap-4 pt-4 lg:grid-cols-4"> <div class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3">
<template x-for="item in filteredApplications" :key="item.id"> <template x-for="item in filteredApplications" :key="item.id">
<span class="relative"> <span class="relative">
<a class="h-24 box group" :href="item.hrefLink"> <a class="h-24 box group" :href="item.hrefLink">

View File

@@ -28,7 +28,7 @@
<div class="w-full pl-8"> <div class="w-full pl-8">
@isset($serviceApplication) @isset($serviceApplication)
<div x-cloak x-show="activeTab === 'general'" class="h-full"> <div x-cloak x-show="activeTab === 'general'" class="h-full">
<livewire:project.service.application :application="$serviceApplication" /> <livewire:project.service.service-application-view :application="$serviceApplication" />
</div> </div>
<div x-cloak x-show="activeTab === 'storages'"> <div x-cloak x-show="activeTab === 'storages'">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">

View File

@@ -11,10 +11,15 @@
<div> <div>
<div class="grid grid-cols-1 gap-2 pb-4 lg:grid-cols-4"> <div class="grid grid-cols-1 gap-2 pb-4 lg:grid-cols-4">
@foreach ($server->destinations() as $destination) @foreach ($server->destinations() as $destination)
<div class="flex flex-col gap-2 box" wire:click="cloneTo('{{ data_get($destination, 'id') }}')"> <x-new-modal action="cloneTo({{ data_get($destination, 'id') }})">
<div class="font-bold text-white">{{ $server->name }}</div> <x:slot name="content">
<div>{{ $destination->name }}</div> <div class="flex flex-col gap-2 box">
</div> <div class="font-bold text-white">{{ $server->name }}</div>
<div>{{ $destination->name }}</div>
</div>
</x:slot>
<div>You are about to clone this resource.</div>
</x-new-modal>
@endforeach @endforeach
</div> </div>
</div> </div>
@@ -32,10 +37,15 @@
@forelse ($projects as $project) @forelse ($projects as $project)
<div class="flex flex-row flex-wrap gap-2"> <div class="flex flex-row flex-wrap gap-2">
@foreach ($project->environments as $environment) @foreach ($project->environments as $environment)
<div class="flex flex-col gap-2 box" wire:click="moveTo('{{ data_get($environment, 'id') }}')"> <x-new-modal action="moveTo({{ data_get($environment, 'id') }})">
<div class="font-bold text-white">{{ $project->name }}</div> <x:slot name="content">
<div><span class="text-warning">{{ $environment->name }}</span> environment</div> <div class="flex flex-col gap-2 box">
</div> <div class="font-bold text-white">{{ $project->name }}</div>
<div><span class="text-warning">{{ $environment->name }}</span> environment</div>
</div>
</x:slot>
<div>You are about to move this resource.</div>
</x-new-modal>
@endforeach @endforeach
</div> </div>
@empty @empty

View File

@@ -4,7 +4,7 @@
@forelse ($this->resource->tags as $tagId => $tag) @forelse ($this->resource->tags as $tagId => $tag)
<div class="px-2 py-1 text-center text-white select-none w-fit bg-coolgray-100 hover:bg-coolgray-200"> <div class="px-2 py-1 text-center text-white select-none w-fit bg-coolgray-100 hover:bg-coolgray-200">
{{ $tag->name }} {{ $tag->name }}
<svg wire:click="deleteTag('{{ $tag->id }}','{{ $tag->name }}')" <svg wire:click="deleteTag('{{ $tag->id }}')"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
class="inline-block w-3 h-3 rounded cursor-pointer stroke-current hover:bg-red-500"> class="inline-block w-3 h-3 rounded cursor-pointer stroke-current hover:bg-red-500">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
@@ -17,16 +17,19 @@
<form wire:submit='submit' class="flex items-end gap-2 pt-4"> <form wire:submit='submit' class="flex items-end gap-2 pt-4">
<div class="w-64"> <div class="w-64">
<x-forms.input label="Create new or assign existing tags" <x-forms.input label="Create new or assign existing tags"
helper="You add more at once with space seperated list: web api something<br><br>If the tag does not exists, it will be created." wire:model="new_tag" /> helper="You add more at once with space seperated list: web api something<br><br>If the tag does not exists, it will be created."
wire:model="new_tag" />
</div> </div>
<x-forms.button type="submit">Add</x-forms.button> <x-forms.button type="submit">Add</x-forms.button>
</form> </form>
<h3 class="pt-4">Already defined tags</h3> @if ($tags->count() > 0)
<div>Click to quickly add</div> <h3 class="pt-4">Already defined tags</h3>
<div class="flex gap-2 pt-4"> <div>Click to quickly add one.</div>
@foreach ($tags as $tag) <div class="flex gap-2 pt-4">
<x-forms.button wire:click="addTag('{{ $tag->id }}','{{ $tag->name }}')"> @foreach ($tags as $tag)
{{ $tag->name }}</x-forms.button> <x-forms.button wire:click="addTag('{{ $tag->id }}','{{ $tag->name }}')">
@endforeach {{ $tag->name }}</x-forms.button>
</div> @endforeach
</div>
@endif
</div> </div>

View File

@@ -1,5 +1,5 @@
<div> <div>
<form wire:submit='submit' class="flex flex-col"> <form wire:submit.prevent='submit' class="flex flex-col">
<div class="flex gap-2"> <div class="flex gap-2">
<h2>General</h2> <h2>General</h2>
@if ($server->id === 0) @if ($server->id === 0)
@@ -18,10 +18,17 @@
Server is reachable and validated. Server is reachable and validated.
@endif @endif
@if ((!$server->settings->is_reachable || !$server->settings->is_usable) && $server->id !== 0) @if ((!$server->settings->is_reachable || !$server->settings->is_usable) && $server->id !== 0)
<x-forms.button class="mt-8 mb-4 font-bold box-without-bg bg-coollabs hover:bg-coollabs-100" <x-slide-over closeWithX fullScreen>
wire:click.prevent='validateServer' isHighlighted> <x-slot:title>Configuring Server</x-slot:title>
Validate Server & Install Docker Engine <x-slot:content>
</x-forms.button> <livewire:server.validate-and-install :server="$server" />
</x-slot:content>
<x-forms.button @click="slideOverOpen=true"
class="w-full mt-8 mb-4 font-bold box-without-bg bg-coollabs hover:bg-coollabs-100"
wire:click.prevent='validateServer' isHighlighted>
Validate Server & Install Docker Engine
</x-forms.button>
</x-slide-over>
@endif @endif
@if ((!$server->settings->is_reachable || !$server->settings->is_usable) && $server->id === 0) @if ((!$server->settings->is_reachable || !$server->settings->is_usable) && $server->id === 0)
<x-forms.button class="mt-8 mb-4 font-bold box-without-bg bg-coollabs hover:bg-coollabs-100" <x-forms.button class="mt-8 mb-4 font-bold box-without-bg bg-coollabs hover:bg-coollabs-100"

View File

@@ -1,14 +1,4 @@
<div> <div>
<x-modal modalId="installDocker">
<x-slot:modalBody>
<livewire:activity-monitor header="Docker Installation Logs" />
</x-slot:modalBody>
<x-slot:modalSubmit>
<x-forms.button onclick="installDocker.close()" type="submit">
Close
</x-forms.button>
</x-slot:modalSubmit>
</x-modal>
<x-server.navbar :server="$server" :parameters="$parameters" /> <x-server.navbar :server="$server" :parameters="$parameters" />
<livewire:server.form :server="$server" /> <livewire:server.form :server="$server" />
<livewire:server.delete :server="$server" /> <livewire:server.delete :server="$server" />

View File

@@ -0,0 +1,88 @@
<div class="flex flex-col gap-2">
@if ($uptime)
<div class="flex w-64 gap-2">Server is reachable: <svg class="w-5 h-5 text-success" viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg">
<g fill="currentColor">
<path
d="m237.66 85.26l-128.4 128.4a8 8 0 0 1-11.32 0l-71.6-72a8 8 0 0 1 0-11.31l24-24a8 8 0 0 1 11.32 0l36.68 35.32a8 8 0 0 0 11.32 0l92.68-91.32a8 8 0 0 1 11.32 0l24 23.6a8 8 0 0 1 0 11.31"
opacity=".2" />
<path
d="m243.28 68.24l-24-23.56a16 16 0 0 0-22.58 0L104 136l-.11-.11l-36.64-35.27a16 16 0 0 0-22.57.06l-24 24a16 16 0 0 0 0 22.61l71.62 72a16 16 0 0 0 22.63 0l128.4-128.38a16 16 0 0 0-.05-22.67M103.62 208L32 136l24-24l.11.11l36.64 35.27a16 16 0 0 0 22.52 0L208.06 56L232 79.6Z" />
</g>
</svg></div>
@else
@if ($error)
<div class="flex w-64 gap-2">Server is reachable: <svg class="w-5 h-5 text-error" viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M208.49 191.51a12 12 0 0 1-17 17L128 145l-63.51 63.49a12 12 0 0 1-17-17L111 128L47.51 64.49a12 12 0 0 1 17-17L128 111l63.51-63.52a12 12 0 0 1 17 17L145 128Z" />
</svg></div>
@else
<div class="w-64"><x-loading text="Server is reachable: " /></div>
@endif
@endif
@if ($uptime)
@if ($supported_os_type)
<div class="flex w-64 gap-2">Supported OS type: <svg class="w-5 h-5 text-success" viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg">
<g fill="currentColor">
<path
d="m237.66 85.26l-128.4 128.4a8 8 0 0 1-11.32 0l-71.6-72a8 8 0 0 1 0-11.31l24-24a8 8 0 0 1 11.32 0l36.68 35.32a8 8 0 0 0 11.32 0l92.68-91.32a8 8 0 0 1 11.32 0l24 23.6a8 8 0 0 1 0 11.31"
opacity=".2" />
<path
d="m243.28 68.24l-24-23.56a16 16 0 0 0-22.58 0L104 136l-.11-.11l-36.64-35.27a16 16 0 0 0-22.57.06l-24 24a16 16 0 0 0 0 22.61l71.62 72a16 16 0 0 0 22.63 0l128.4-128.38a16 16 0 0 0-.05-22.67M103.62 208L32 136l24-24l.11.11l36.64 35.27a16 16 0 0 0 22.52 0L208.06 56L232 79.6Z" />
</g>
</svg></div>
@else
@if ($error)
<div class="flex w-64 gap-2">Server is reachable: <svg class="w-5 h-5 text-error" viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M208.49 191.51a12 12 0 0 1-17 17L128 145l-63.51 63.49a12 12 0 0 1-17-17L111 128L47.51 64.49a12 12 0 0 1 17-17L128 111l63.51-63.52a12 12 0 0 1 17 17L145 128Z" />
</svg></div>
@else
<div class="w-64"><x-loading text="Server is reachable:" /></div>
@endif
@endif
@endif
@if ($uptime && $supported_os_type)
@if ($docker_installed)
<div class="flex w-64 gap-2">Docker is installed: <svg class="w-5 h-5 text-success" viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg">
<g fill="currentColor">
<path
d="m237.66 85.26l-128.4 128.4a8 8 0 0 1-11.32 0l-71.6-72a8 8 0 0 1 0-11.31l24-24a8 8 0 0 1 11.32 0l36.68 35.32a8 8 0 0 0 11.32 0l92.68-91.32a8 8 0 0 1 11.32 0l24 23.6a8 8 0 0 1 0 11.31"
opacity=".2" />
<path
d="m243.28 68.24l-24-23.56a16 16 0 0 0-22.58 0L104 136l-.11-.11l-36.64-35.27a16 16 0 0 0-22.57.06l-24 24a16 16 0 0 0 0 22.61l71.62 72a16 16 0 0 0 22.63 0l128.4-128.38a16 16 0 0 0-.05-22.67M103.62 208L32 136l24-24l.11.11l36.64 35.27a16 16 0 0 0 22.52 0L208.06 56L232 79.6Z" />
</g>
</svg></div>
@else
@if ($error)
<div class="flex w-64 gap-2">Docker is installed: <svg class="w-5 h-5 text-error" viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M208.49 191.51a12 12 0 0 1-17 17L128 145l-63.51 63.49a12 12 0 0 1-17-17L111 128L47.51 64.49a12 12 0 0 1 17-17L128 111l63.51-63.52a12 12 0 0 1 17 17L145 128Z" />
</svg></div>
@else
<div class="w-64"><x-loading text="Docker is installed:" /></div>
@endif
@endif
@endif
@isset($docker_version)
<div class="flex w-64 gap-2">Minimum Docker version installed: <svg class="w-5 h-5 text-success"
viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<g fill="currentColor">
<path
d="m237.66 85.26l-128.4 128.4a8 8 0 0 1-11.32 0l-71.6-72a8 8 0 0 1 0-11.31l24-24a8 8 0 0 1 11.32 0l36.68 35.32a8 8 0 0 0 11.32 0l92.68-91.32a8 8 0 0 1 11.32 0l24 23.6a8 8 0 0 1 0 11.31"
opacity=".2" />
<path
d="m243.28 68.24l-24-23.56a16 16 0 0 0-22.58 0L104 136l-.11-.11l-36.64-35.27a16 16 0 0 0-22.57.06l-24 24a16 16 0 0 0 0 22.61l71.62 72a16 16 0 0 0 22.63 0l128.4-128.38a16 16 0 0 0-.05-22.67M103.62 208L32 136l24-24l.11.11l36.64 35.27a16 16 0 0 0 22.52 0L208.06 56L232 79.6Z" />
</g>
</svg></div>
@endisset
<livewire:new-activity-monitor header="Logs" />
@isset($error)
<pre class="font-bold whitespace-pre-line text-error">{!! $error !!}</pre>
@endisset
</div>

View File

@@ -1,11 +1,14 @@
<div> <div>
<h1>Tags</h1> <h1>Tags</h1>
<div>Here you can see all the tags here</div> <div class="flex flex-col gap-2 pb-6 ">
<div class="flex gap-2 pt-10"> <div>Available tags: </div>
@forelse ($tags as $tag) <div class="flex flex-wrap gap-2">
<a class="box" href="{{ route('tags.show', ['tag_name' => $tag->name]) }}">{{ $tag->name }}</a> @forelse ($tags as $oneTag)
@empty <a class="flex items-center justify-center h-6 px-2 text-white min-w-14 w-fit hover:no-underline hover:bg-coolgray-200 bg-coolgray-100"
<div>No tags yet defined yet. Go to a resource and add a tag there.</div> href="{{ route('tags.show', ['tag_name' => $oneTag->name]) }}">{{ $oneTag->name }}</a>
@endforelse @empty
<div>No tags yet defined yet. Go to a resource and add a tag there.</div>
@endforelse
</div>
</div> </div>
</div> </div>

View File

@@ -1,24 +1,46 @@
<div> <div>
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<div> <div>
<h1>Tag: {{ $tag->name }}</h1> <h1>Tags</h1>
<div class="pt-2">Tag details</div>
</div> </div>
</div> </div>
<div class="pt-4"> <div class="flex flex-col gap-2 pb-6 ">
<div>Available tags: </div>
<div class="flex flex-wrap gap-2 ">
@forelse ($tags as $oneTag)
<a :class="{{ $tag->id == $oneTag->id }} && 'bg-coollabs hover:bg-coollabs-100'"
class="flex items-center justify-center h-6 px-2 text-white min-w-14 w-fit hover:no-underline hover:bg-coolgray-200 bg-coolgray-100"
href="{{ route('tags.show', ['tag_name' => $oneTag->name]) }}">{{ $oneTag->name }}</a>
@empty
<div>No tags yet defined yet. Go to a resource and add a tag there.</div>
@endforelse
</div>
</div>
<div>
<h3 class="py-4">Details</h3>
<div class="flex items-end gap-2 "> <div class="flex items-end gap-2 ">
<div class="w-[500px]"> <div class="w-[500px]">
<x-forms.input readonly label="Deploy Webhook URL" id="webhook" /> <x-forms.input readonly label="Deploy Webhook URL" id="webhook" />
</div> </div>
<x-new-modal buttonTitle="Redeploy All" action="redeploy_all" class="mt-1"> <x-new-modal isHighlighted buttonTitle="Redeploy All" action="redeploy_all">
All resources will be redeployed. All resources will be redeployed.
</x-new-modal> </x-new-modal>
</div> </div>
<div class="grid gap-2 pt-4 lg:grid-cols-4"> <div class="grid grid-cols-1 gap-2 pt-4 lg:grid-cols-2 xl:grid-cols-3">
@foreach ($resources as $resource) @foreach ($applications as $application)
<a href="{{ $resource->link() }}" class="flex flex-col box group"> <a href="{{ $application->link() }}" class="flex flex-col box group">
<span class="font-bold text-white">{{ $resource->name }}</span> <span
<span class="description">{{ $resource->description }}</span> class="font-bold text-white">{{ $application->project()->name }}/{{ $application->environment->name }}</span>
<span class="text-white ">{{ $application->name }}</span>
<span class="description">{{ $application->description }}</span>
</a>
@endforeach
@foreach ($services as $service)
<a href="{{ $service->link() }}" class="flex flex-col box group">
<span
class="font-bold text-white">{{ $service->project()->name }}/{{ $service->environment->name }}</span>
<span class="text-white ">{{ $service->name }}</span>
<span class="description">{{ $service->description }}</span>
</a> </a>
@endforeach @endforeach
</div> </div>

View File

@@ -12,6 +12,7 @@ use App\Models\Tag;
use App\Models\User; use App\Models\User;
use App\Providers\RouteServiceProvider; use App\Providers\RouteServiceProvider;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@@ -34,6 +35,16 @@ if (isDev()) {
Route::get('/health', function () { Route::get('/health', function () {
return 'OK'; return 'OK';
}); });
Route::post('/feedback', function (Request $request) {
$content = $request->input('content');
$webhook_url = config('coolify.feedback_discord_webhook');
if ($webhook_url) {
Http::post($webhook_url, [
'content' => $content
]);
}
return response()->json(['message' => 'Feedback sent.'], 200);
});
// Route::group([ // Route::group([
// 'middleware' => $middlewares, // 'middleware' => $middlewares,
// 'prefix' => 'v1' // 'prefix' => 'v1'
@@ -53,6 +64,8 @@ Route::group([
'prefix' => 'v1' 'prefix' => 'v1'
], function () { ], function () {
Route::get('/deploy', [Deploy::class, 'deploy']); Route::get('/deploy', [Deploy::class, 'deploy']);
}); });
Route::middleware(['throttle:5'])->group(function () { Route::middleware(['throttle:5'])->group(function () {

View File

@@ -764,7 +764,6 @@ Route::post('/payments/stripe/events', function () {
]); ]);
$type = data_get($event, 'type'); $type = data_get($event, 'type');
$data = data_get($event, 'data.object'); $data = data_get($event, 'data.object');
ray('Event: ' . $type);
switch ($type) { switch ($type) {
case 'checkout.session.completed': case 'checkout.session.completed':
$clientReferenceId = data_get($data, 'client_reference_id'); $clientReferenceId = data_get($data, 'client_reference_id');
@@ -779,7 +778,8 @@ Route::post('/payments/stripe/events', function () {
$team = Team::find($teamId); $team = Team::find($teamId);
$found = $team->members->where('id', $userId)->first(); $found = $team->members->where('id', $userId)->first();
if (!$found->isAdmin()) { if (!$found->isAdmin()) {
throw new Exception("User {$userId} is not an admin or owner of team {$team->id}."); send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
throw new Exception("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
} }
$subscription = Subscription::where('team_id', $teamId)->first(); $subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) { if ($subscription) {
@@ -819,21 +819,35 @@ Route::post('/payments/stripe/events', function () {
break; break;
case 'invoice.payment_failed': case 'invoice.payment_failed':
$customerId = data_get($data, 'customer'); $customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (!$subscription) {
send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: ' . $customerId);
return response('No subscription found in Coolify.');
}
$team = data_get($subscription, 'team'); $team = data_get($subscription, 'team');
if (!$team) { if (!$team) {
throw new Exception('No team found for subscription: ' . $subscription->id); send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: ' . $customerId);
return response('No team found in Coolify.');
}
if (!$subscription->stripe_invoice_paid) {
SubscriptionInvoiceFailedJob::dispatch($team);
send_internal_notification('Invoice payment failed: ' . $customerId);
} else {
send_internal_notification('Invoice payment failed but already paid: ' . $customerId);
} }
SubscriptionInvoiceFailedJob::dispatch($team);
send_internal_notification('Invoice payment failed: ' . $subscription->team->id);
break; break;
case 'payment_intent.payment_failed': case 'payment_intent.payment_failed':
$customerId = data_get($data, 'customer'); $customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
$subscription->update([ if (!$subscription) {
'stripe_invoice_paid' => false, send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: ' . $customerId);
]); return response('No subscription found in Coolify.');
send_internal_notification('Subscription payment failed: ' . $subscription->team->id); }
if ($subscription->stripe_invoice_paid) {
send_internal_notification('payment_intent.payment_failed but invoice is active for customer: ' . $customerId);
return;
}
send_internal_notification('Subscription payment failed for customer: ' . $customerId);
break; break;
case 'customer.subscription.updated': case 'customer.subscription.updated':
$customerId = data_get($data, 'customer'); $customerId = data_get($data, 'customer');
@@ -864,7 +878,7 @@ Route::post('/payments/stripe/events', function () {
$subscription->update([ $subscription->update([
'stripe_invoice_paid' => false, 'stripe_invoice_paid' => false,
]); ]);
send_internal_notification('Subscription paused or incomplete for team: ' . $subscription->team->id); send_internal_notification('Subscription paused or incomplete for customer: ' . $customerId);
} }
// Trial ended but subscribed, reactive servers // Trial ended but subscribed, reactive servers
@@ -874,7 +888,7 @@ Route::post('/payments/stripe/events', function () {
} }
if ($feedback) { if ($feedback) {
$reason = "Cancellation feedback for {$subscription->team->id}: '" . $feedback . "'"; $reason = "Cancellation feedback for {$customerId}: '" . $feedback . "'";
if ($comment) { if ($comment) {
$reason .= ' with comment: \'' . $comment . "'"; $reason .= ' with comment: \'' . $comment . "'";
} }
@@ -884,7 +898,7 @@ Route::post('/payments/stripe/events', function () {
if ($cancelAtPeriodEnd) { if ($cancelAtPeriodEnd) {
// send_internal_notification('Subscription cancelled at period end for team: ' . $subscription->team->id); // send_internal_notification('Subscription cancelled at period end for team: ' . $subscription->team->id);
} else { } else {
send_internal_notification('Subscription resumed for team: ' . $subscription->team->id); send_internal_notification('customer.subscription.updated for customer: ' . $customerId);
} }
} }
break; break;
@@ -901,9 +915,10 @@ Route::post('/payments/stripe/events', function () {
'stripe_invoice_paid' => false, 'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => true, 'stripe_trial_already_ended' => true,
]); ]);
// send_internal_notification('Subscription cancelled: ' . $subscription->team->id); send_internal_notification('customer.subscription.deleted for customer: ' . $customerId);
break; break;
case 'customer.subscription.trial_will_end': case 'customer.subscription.trial_will_end':
// Not used for now
$customerId = data_get($data, 'customer'); $customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$team = data_get($subscription, 'team'); $team = data_get($subscription, 'team');
@@ -925,7 +940,7 @@ Route::post('/payments/stripe/events', function () {
'stripe_invoice_paid' => false, 'stripe_invoice_paid' => false,
]); ]);
SubscriptionTrialEndedJob::dispatch($team); SubscriptionTrialEndedJob::dispatch($team);
send_internal_notification('Subscription paused for team: ' . $subscription->team->id); send_internal_notification('Subscription paused for customer: ' . $customerId);
break; break;
default: default:
// Unhandled event type // Unhandled event type

View File

@@ -4,7 +4,7 @@
"version": "3.12.36" "version": "3.12.36"
}, },
"v4": { "v4": {
"version": "4.0.0-beta.205" "version": "4.0.0-beta.211"
} }
} }
} }