Merge branch 'next' into feat/deployment-token

This commit is contained in:
Kael
2024-11-04 23:33:26 +11:00
committed by GitHub
76 changed files with 1463 additions and 942 deletions

View File

@@ -30,7 +30,7 @@ class GetContainersStatus
$this->containerReplicates = $containerReplicates; $this->containerReplicates = $containerReplicates;
$this->server = $server; $this->server = $server;
if (! $this->server->isFunctional()) { if (! $this->server->isFunctional()) {
return 'Server is not ready.'; return 'Server is not functional.';
} }
$this->applications = $this->server->applications(); $this->applications = $this->server->applications();
$skip_these_applications = collect([]); $skip_these_applications = collect([]);

View File

@@ -40,7 +40,7 @@ class CreateNewUser implements CreatesNewUsers
$user = User::create([ $user = User::create([
'id' => 0, 'id' => 0,
'name' => $input['name'], 'name' => $input['name'],
'email' => $input['email'], 'email' => strtolower($input['email']),
'password' => Hash::make($input['password']), 'password' => Hash::make($input['password']),
]); ]);
$team = $user->teams()->first(); $team = $user->teams()->first();
@@ -52,7 +52,7 @@ class CreateNewUser implements CreatesNewUsers
} else { } else {
$user = User::create([ $user = User::create([
'name' => $input['name'], 'name' => $input['name'],
'email' => $input['email'], 'email' => strtolower($input['email']),
'password' => Hash::make($input['password']), 'password' => Hash::make($input['password']),
]); ]);
$team = $user->teams()->first(); $team = $user->teams()->first();

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Actions\Server;
use App\Models\Application;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Lorisleiva\Actions\Concerns\AsAction;
class ResourcesCheck
{
use AsAction;
public function handle()
{
$seconds = 60;
try {
Application::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
ServiceApplication::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
ServiceDatabase::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
StandalonePostgresql::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
StandaloneRedis::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
StandaloneMongodb::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
StandaloneMysql::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
StandaloneMariadb::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
StandaloneKeydb::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
StandaloneDragonfly::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
StandaloneClickhouse::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']);
} catch (\Throwable $e) {
return handleError($e);
}
}
}

View File

@@ -0,0 +1,269 @@
<?php
namespace App\Actions\Server;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Jobs\CheckAndStartSentinelJob;
use App\Jobs\ServerStorageCheckJob;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Notifications\Container\ContainerRestarted;
use Arr;
use Lorisleiva\Actions\Concerns\AsAction;
class ServerCheck
{
use AsAction;
public Server $server;
public bool $isSentinel = false;
public $containers;
public $databases;
public function handle(Server $server, $data = null)
{
$this->server = $server;
try {
if ($this->server->isFunctional() === false) {
return 'Server is not functional.';
}
if (! $this->server->isSwarmWorker() && ! $this->server->isBuildServer()) {
if (isset($data)) {
$data = collect($data);
$this->server->sentinelHeartbeat();
$this->containers = collect(data_get($data, 'containers'));
$filesystemUsageRoot = data_get($data, 'filesystem_usage_root.used_percentage');
ServerStorageCheckJob::dispatch($this->server, $filesystemUsageRoot);
$containerReplicates = null;
$this->isSentinel = true;
} else {
['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers();
// ServerStorageCheckJob::dispatch($this->server);
}
if (is_null($this->containers)) {
return 'No containers found.';
}
if (isset($containerReplicates)) {
foreach ($containerReplicates as $containerReplica) {
$name = data_get($containerReplica, 'Name');
$this->containers = $this->containers->map(function ($container) use ($name, $containerReplica) {
if (data_get($container, 'Spec.Name') === $name) {
$replicas = data_get($containerReplica, 'Replicas');
$running = str($replicas)->explode('/')[0];
$total = str($replicas)->explode('/')[1];
if ($running === $total) {
data_set($container, 'State.Status', 'running');
data_set($container, 'State.Health.Status', 'healthy');
} else {
data_set($container, 'State.Status', 'starting');
data_set($container, 'State.Health.Status', 'unhealthy');
}
}
return $container;
});
}
}
$this->checkContainers();
if ($this->server->isSentinelEnabled() && $this->isSentinel === false) {
CheckAndStartSentinelJob::dispatch($this->server);
}
if ($this->server->isLogDrainEnabled()) {
$this->checkLogDrainContainer();
}
if ($this->server->proxySet() && ! $this->server->proxy->force_stop) {
$foundProxyContainer = $this->containers->filter(function ($value, $key) {
if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
} else {
return data_get($value, 'Name') === '/coolify-proxy';
}
})->first();
if (! $foundProxyContainer) {
try {
$shouldStart = CheckProxy::run($this->server);
if ($shouldStart) {
StartProxy::run($this->server, false);
$this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
}
} catch (\Throwable $e) {
}
} else {
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
$this->server->save();
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
}
}
}
} catch (\Throwable $e) {
return handleError($e);
}
}
private function checkLogDrainContainer()
{
$foundLogDrainContainer = $this->containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-log-drain';
})->first();
if ($foundLogDrainContainer) {
$status = data_get($foundLogDrainContainer, 'State.Status');
if ($status !== 'running') {
StartLogDrain::dispatch($this->server);
}
} else {
StartLogDrain::dispatch($this->server);
}
}
private function checkContainers()
{
foreach ($this->containers as $container) {
if ($this->isSentinel) {
$labels = Arr::undot(data_get($container, 'labels'));
} else {
if ($this->server->isSwarm()) {
$labels = Arr::undot(data_get($container, 'Spec.Labels'));
} else {
$labels = Arr::undot(data_get($container, 'Config.Labels'));
}
}
$managed = data_get($labels, 'coolify.managed');
if (! $managed) {
continue;
}
$uuid = data_get($labels, 'coolify.name');
if (! $uuid) {
$uuid = data_get($labels, 'com.docker.compose.service');
}
if ($this->isSentinel) {
$containerStatus = data_get($container, 'state');
$containerHealth = data_get($container, 'health_status');
} else {
$containerStatus = data_get($container, 'State.Status');
$containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
}
$containerStatus = "$containerStatus ($containerHealth)";
$applicationId = data_get($labels, 'coolify.applicationId');
$serviceId = data_get($labels, 'coolify.serviceId');
$databaseId = data_get($labels, 'coolify.databaseId');
$pullRequestId = data_get($labels, 'coolify.pullRequestId');
if ($applicationId) {
// Application
if ($pullRequestId != 0) {
if (str($applicationId)->contains('-')) {
$applicationId = str($applicationId)->before('-');
}
$preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
if ($preview) {
$preview->update(['status' => $containerStatus]);
}
} else {
$application = Application::where('id', $applicationId)->first();
if ($application) {
$application->update([
'status' => $containerStatus,
'last_online_at' => now(),
]);
}
}
} elseif (isset($serviceId)) {
// Service
$subType = data_get($labels, 'coolify.service.subType');
$subId = data_get($labels, 'coolify.service.subId');
$service = Service::where('id', $serviceId)->first();
if (! $service) {
continue;
}
if ($subType === 'application') {
$service = ServiceApplication::where('id', $subId)->first();
} else {
$service = ServiceDatabase::where('id', $subId)->first();
}
if ($service) {
$service->update([
'status' => $containerStatus,
'last_online_at' => now(),
]);
if ($subType === 'database') {
$isPublic = data_get($service, 'is_public');
if ($isPublic) {
$foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
if ($this->isSentinel) {
return data_get($value, 'name') === $uuid.'-proxy';
} else {
if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
} else {
return data_get($value, 'Name') === "/$uuid-proxy";
}
}
})->first();
if (! $foundTcpProxy) {
StartDatabaseProxy::run($service);
}
}
}
}
} else {
// Database
if (is_null($this->databases)) {
$this->databases = $this->server->databases();
}
$database = $this->databases->where('uuid', $uuid)->first();
if ($database) {
$database->update([
'status' => $containerStatus,
'last_online_at' => now(),
]);
$isPublic = data_get($database, 'is_public');
if ($isPublic) {
$foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
if ($this->isSentinel) {
return data_get($value, 'name') === $uuid.'-proxy';
} else {
if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
} else {
return data_get($value, 'Name') === "/$uuid-proxy";
}
}
})->first();
if (! $foundTcpProxy) {
StartDatabaseProxy::run($database);
$this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
}
}
}
}
}
}
}

View File

@@ -10,6 +10,7 @@ use App\Models\Environment;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\Server; use App\Models\Server;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\User;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
@@ -41,6 +42,7 @@ class Init extends Command
$this->disable_metrics(); $this->disable_metrics();
$this->replace_slash_in_environment_name(); $this->replace_slash_in_environment_name();
$this->restore_coolify_db_backup(); $this->restore_coolify_db_backup();
$this->update_user_emails();
// //
$this->update_traefik_labels(); $this->update_traefik_labels();
if (! isCloud() || $this->option('force-cloud')) { if (! isCloud() || $this->option('force-cloud')) {
@@ -92,6 +94,15 @@ class Init extends Command
} }
} }
private function update_user_emails()
{
try {
User::whereRaw('email ~ \'[A-Z]\'')->get()->each(fn (User $user) => $user->update(['email' => strtolower($user->email)]));
} catch (\Throwable $e) {
echo "Error in updating user emails: {$e->getMessage()}\n";
}
}
private function update_traefik_labels() private function update_traefik_labels()
{ {
try { try {

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Console\Commands;
use App\Actions\Server\ServerCheck;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Models\Server;
use Illuminate\Console\Command;
use Str;
class Weird extends Command
{
protected $signature = 'weird {--number=1} {--run}';
protected $description = 'Weird stuff';
public function handle()
{
try {
if (! isDev()) {
$this->error('This command can only be run in development mode');
return;
}
$run = $this->option('run');
if ($run) {
$servers = Server::all();
foreach ($servers as $server) {
ServerCheck::dispatch($server);
}
return;
}
$number = $this->option('number');
for ($i = 0; $i < $number; $i++) {
$uuid = Str::uuid();
$server = Server::create([
'name' => 'localhost-'.$uuid,
'description' => 'This is a test docker container in development mode',
'ip' => 'coolify-testing-host',
'team_id' => 0,
'private_key_id' => 1,
'proxy' => [
'type' => ProxyTypes::NONE->value,
'status' => ProxyStatus::EXITED->value,
],
]);
$server->settings->update([
'is_usable' => true,
'is_reachable' => true,
]);
}
} catch (\Exception $e) {
$this->error($e->getMessage());
}
}
}

View File

@@ -13,6 +13,7 @@ use App\Jobs\PullTemplatesFromCDN;
use App\Jobs\ScheduledTaskJob; use App\Jobs\ScheduledTaskJob;
use App\Jobs\ServerCheckJob; use App\Jobs\ServerCheckJob;
use App\Jobs\ServerCleanupMux; use App\Jobs\ServerCleanupMux;
use App\Jobs\ServerStorageCheckJob;
use App\Jobs\UpdateCoolifyJob; use App\Jobs\UpdateCoolifyJob;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
@@ -31,7 +32,7 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule): void protected function schedule(Schedule $schedule): void
{ {
$this->allServers = Server::where('ip', '!=', '1.2.3.4')->get(); $this->allServers = Server::where('ip', '!=', '1.2.3.4');
$this->settings = instanceSettings(); $this->settings = instanceSettings();
@@ -41,13 +42,16 @@ class Kernel extends ConsoleKernel
// Instance Jobs // Instance Jobs
$schedule->command('horizon:snapshot')->everyMinute(); $schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer(); $schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
$schedule->job(new CheckHelperImageJob)->everyFiveMinutes()->onOneServer();
// Server Jobs // Server Jobs
$this->checkScheduledBackups($schedule);
$this->checkResources($schedule); $this->checkResources($schedule);
$this->checkScheduledBackups($schedule);
$this->checkScheduledTasks($schedule); $this->checkScheduledTasks($schedule);
$schedule->command('uploads:clear')->everyTwoMinutes(); $schedule->command('uploads:clear')->everyTwoMinutes();
$schedule->job(new CheckHelperImageJob)->everyFiveMinutes()->onOneServer();
} else { } else {
// Instance Jobs // Instance Jobs
$schedule->command('horizon:snapshot')->everyFiveMinutes(); $schedule->command('horizon:snapshot')->everyFiveMinutes();
@@ -57,9 +61,11 @@ class Kernel extends ConsoleKernel
$this->scheduleUpdates($schedule); $this->scheduleUpdates($schedule);
// Server Jobs // Server Jobs
$this->checkScheduledBackups($schedule);
$this->checkResources($schedule); $this->checkResources($schedule);
$this->pullImages($schedule); $this->pullImages($schedule);
$this->checkScheduledBackups($schedule);
$this->checkScheduledTasks($schedule); $this->checkScheduledTasks($schedule);
$schedule->command('cleanup:database --yes')->daily(); $schedule->command('cleanup:database --yes')->daily();
@@ -69,7 +75,7 @@ class Kernel extends ConsoleKernel
private function pullImages($schedule): void private function pullImages($schedule): void
{ {
$servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true); $servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get();
foreach ($servers as $server) { foreach ($servers as $server) {
if ($server->isSentinelEnabled()) { if ($server->isSentinelEnabled()) {
$schedule->job(function () use ($server) { $schedule->job(function () use ($server) {
@@ -103,23 +109,33 @@ class Kernel extends ConsoleKernel
private function checkResources($schedule): void private function checkResources($schedule): void
{ {
if (isCloud()) { if (isCloud()) {
$servers = $this->allServers->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false); $servers = $this->allServers->whereHas('team.subscription')->get();
$own = Team::find(0)->servers; $own = Team::find(0)->servers;
$servers = $servers->merge($own); $servers = $servers->merge($own);
} else { } else {
$servers = $this->allServers; $servers = $this->allServers->get();
} }
// $schedule->job(new \App\Jobs\ResourcesCheck)->everyMinute()->onOneServer();
foreach ($servers as $server) { foreach ($servers as $server) {
$lastSentinelUpdate = $server->sentinel_updated_at;
$serverTimezone = $server->settings->server_timezone; $serverTimezone = $server->settings->server_timezone;
// Sentinel check
$lastSentinelUpdate = $server->sentinel_updated_at;
if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) { if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) {
// Check container status every minute if Sentinel does not activated
$schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer(); $schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
// $schedule->job(new \App\Jobs\ServerCheckNewJob($server))->everyMinute()->onOneServer();
// Check storage usage every 10 minutes if Sentinel does not activated
$schedule->job(new ServerStorageCheckJob($server))->everyTenMinutes()->onOneServer();
} }
if ($server->settings->force_docker_cleanup) { if ($server->settings->force_docker_cleanup) {
$schedule->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer(); $schedule->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer();
} else { } else {
$schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->onOneServer(); $schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->onOneServer();
} }
// Cleanup multiplexed connections every hour // Cleanup multiplexed connections every hour
$schedule->job(new ServerCleanupMux($server))->hourly()->onOneServer(); $schedule->job(new ServerCleanupMux($server))->hourly()->onOneServer();
@@ -134,14 +150,11 @@ class Kernel extends ConsoleKernel
private function checkScheduledBackups($schedule): void private function checkScheduledBackups($schedule): void
{ {
$scheduled_backups = ScheduledDatabaseBackup::all(); $scheduled_backups = ScheduledDatabaseBackup::where('enabled', true)->get();
if ($scheduled_backups->isEmpty()) { if ($scheduled_backups->isEmpty()) {
return; return;
} }
foreach ($scheduled_backups as $scheduled_backup) { foreach ($scheduled_backups as $scheduled_backup) {
if (! $scheduled_backup->enabled) {
continue;
}
if (is_null(data_get($scheduled_backup, 'database'))) { if (is_null(data_get($scheduled_backup, 'database'))) {
$scheduled_backup->delete(); $scheduled_backup->delete();
@@ -150,7 +163,7 @@ class Kernel extends ConsoleKernel
$server = $scheduled_backup->server(); $server = $scheduled_backup->server();
if (! $server) { if (is_null($server)) {
continue; continue;
} }
$serverTimezone = $server->settings->server_timezone; $serverTimezone = $server->settings->server_timezone;

View File

@@ -230,7 +230,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue->update([ $this->application_deployment_queue->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]); ]);
if (! $this->server->isFunctional()) { if ($this->server->isFunctional() === false) {
$this->application_deployment_queue->addLogEntry('Server is not functional.'); $this->application_deployment_queue->addLogEntry('Server is not functional.');
$this->fail('Server is not functional.'); $this->fail('Server is not functional.');

View File

@@ -39,7 +39,6 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
if (is_null($this->containers)) { if (is_null($this->containers)) {
return 'No containers found.'; return 'No containers found.';
} }
ServerStorageCheckJob::dispatch($this->server);
GetContainersStatus::run($this->server, $this->containers, $containerReplicates); GetContainersStatus::run($this->server, $this->containers, $containerReplicates);
if ($this->server->isSentinelEnabled()) { if ($this->server->isSentinelEnabled()) {

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Jobs;
use App\Actions\Server\ServerCheck;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ServerCheckNewJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public $timeout = 60;
public function __construct(public Server $server) {}
public function handle()
{
try {
ServerCheck::run($this->server);
} catch (\Throwable $e) {
return handleError($e);
}
}
}

View File

@@ -30,8 +30,8 @@ class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue
public function handle() public function handle()
{ {
try { try {
if (! $this->server->isFunctional()) { if ($this->server->isFunctional() === false) {
return 'Server is not ready.'; return 'Server is not functional.';
} }
$team = data_get($this->server, 'team'); $team = data_get($this->server, 'team');
$serverDiskUsageNotificationThreshold = data_get($this->server, 'settings.server_disk_usage_notification_threshold'); $serverDiskUsageNotificationThreshold = data_get($this->server, 'settings.server_disk_usage_notification_threshold');

View File

@@ -3,16 +3,19 @@
namespace App\Livewire\Admin; namespace App\Livewire\Admin;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Livewire\Component; use Livewire\Component;
class Index extends Component class Index extends Component
{ {
public $active_subscribers = []; public int $activeSubscribers;
public $inactive_subscribers = []; public int $inactiveSubscribers;
public $search = ''; public Collection $foundUsers;
public string $search = '';
public function mount() public function mount()
{ {
@@ -29,39 +32,21 @@ class Index extends Component
public function submitSearch() public function submitSearch()
{ {
if ($this->search !== '') { if ($this->search !== '') {
$this->inactive_subscribers = User::whereDoesntHave('teams', function ($query) { $this->foundUsers = User::where(function ($query) {
$query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
})->where(function ($query) {
$query->where('name', 'like', "%{$this->search}%") $query->where('name', 'like', "%{$this->search}%")
->orWhere('email', 'like', "%{$this->search}%"); ->orWhere('email', 'like', "%{$this->search}%");
})->get()->filter(function ($user) { })->get();
return $user->id !== 0;
});
$this->active_subscribers = User::whereHas('teams', function ($query) {
$query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
})->where(function ($query) {
$query->where('name', 'like', "%{$this->search}%")
->orWhere('email', 'like', "%{$this->search}%");
})->get()->filter(function ($user) {
return $user->id !== 0;
});
} else {
$this->getSubscribers();
} }
} }
public function getSubscribers() public function getSubscribers()
{ {
$this->inactive_subscribers = User::whereDoesntHave('teams', function ($query) { $this->inactiveSubscribers = User::whereDoesntHave('teams', function ($query) {
$query->whereRelation('subscription', 'stripe_subscription_id', '!=', null); $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
})->get()->filter(function ($user) { })->count();
return $user->id !== 0; $this->activeSubscribers = User::whereHas('teams', function ($query) {
});
$this->active_subscribers = User::whereHas('teams', function ($query) {
$query->whereRelation('subscription', 'stripe_subscription_id', '!=', null); $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
})->get()->filter(function ($user) { })->count();
return $user->id !== 0;
});
} }
public function switchUser(int $user_id) public function switchUser(int $user_id)

View File

@@ -16,28 +16,28 @@ class Dashboard extends Component
public Collection $servers; public Collection $servers;
public Collection $private_keys; public Collection $privateKeys;
public $deployments_per_server; public array $deploymentsPerServer = [];
public function mount() public function mount()
{ {
$this->private_keys = PrivateKey::ownedByCurrentTeam()->get(); $this->privateKeys = PrivateKey::ownedByCurrentTeam()->get();
$this->servers = Server::ownedByCurrentTeam()->get(); $this->servers = Server::ownedByCurrentTeam()->get();
$this->projects = Project::ownedByCurrentTeam()->get(); $this->projects = Project::ownedByCurrentTeam()->get();
$this->get_deployments(); $this->loadDeployments();
} }
public function cleanup_queue() public function cleanupQueue()
{ {
Artisan::queue('cleanup:deployment-queue', [ Artisan::queue('cleanup:deployment-queue', [
'--team-id' => currentTeam()->id, '--team-id' => currentTeam()->id,
]); ]);
} }
public function get_deployments() public function loadDeployments()
{ {
$this->deployments_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $this->servers->pluck('id'))->get([ $this->deploymentsPerServer = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $this->servers->pluck('id'))->get([
'id', 'id',
'application_id', 'application_id',
'application_name', 'application_name',

View File

@@ -1,46 +0,0 @@
<?php
namespace App\Livewire\Destination;
use Livewire\Component;
class Form extends Component
{
public mixed $destination;
protected $rules = [
'destination.name' => 'required',
'destination.network' => 'required',
'destination.server.ip' => 'required',
];
protected $validationAttributes = [
'destination.name' => 'name',
'destination.network' => 'network',
'destination.server.ip' => 'IP Address/Domain',
];
public function submit()
{
$this->validate();
$this->destination->save();
}
public function delete()
{
try {
if ($this->destination->getMorphClass() === \App\Models\StandaloneDocker::class) {
if ($this->destination->attachedTo()) {
return $this->dispatch('error', 'You must delete all resources before deleting this destination.');
}
instant_remote_process(["docker network disconnect {$this->destination->network} coolify-proxy"], $this->destination->server, throwError: false);
instant_remote_process(['docker network rm -f '.$this->destination->network], $this->destination->server);
}
$this->destination->delete();
return redirect()->route('destination.all');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Livewire\Destination;
use App\Models\Server;
use Livewire\Attributes\Locked;
use Livewire\Component;
class Index extends Component
{
#[Locked]
public $servers;
public function mount()
{
$this->servers = Server::isUsable()->get();
}
public function render()
{
return view('livewire.destination.index');
}
}

View File

@@ -3,111 +3,89 @@
namespace App\Livewire\Destination\New; namespace App\Livewire\Destination\New;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker as ModelsStandaloneDocker; use App\Models\StandaloneDocker;
use App\Models\SwarmDocker; use App\Models\SwarmDocker;
use Illuminate\Support\Collection; use Livewire\Attributes\Locked;
use Livewire\Attributes\Rule;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
class Docker extends Component class Docker extends Component
{ {
#[Locked]
public $servers;
#[Locked]
public Server $selectedServer;
#[Rule(['required', 'string'])]
public string $name; public string $name;
#[Rule(['required', 'string'])]
public string $network; public string $network;
public ?Collection $servers = null; #[Rule(['required', 'string'])]
public string $serverId;
public Server $server; #[Rule(['required', 'boolean'])]
public bool $isSwarm = false;
public ?int $server_id = null; public function mount(?string $server_id = null)
public bool $is_swarm = false;
protected $rules = [
'name' => 'required|string',
'network' => 'required|string',
'server_id' => 'required|integer',
'is_swarm' => 'boolean',
];
protected $validationAttributes = [
'name' => 'name',
'network' => 'network',
'server_id' => 'server',
'is_swarm' => 'swarm',
];
public function mount()
{ {
if (is_null($this->servers)) {
$this->servers = Server::isReachable()->get();
}
if (request()->query('server_id')) {
$this->server_id = request()->query('server_id');
} else {
if ($this->servers->count() > 0) {
$this->server_id = $this->servers->first()->id;
}
}
if (request()->query('network_name')) {
$this->network = request()->query('network_name');
} else {
$this->network = new Cuid2; $this->network = new Cuid2;
$this->servers = Server::isUsable()->get();
if ($server_id) {
$this->selectedServer = $this->servers->find($server_id);
} else {
$this->selectedServer = $this->servers->first();
} }
if ($this->servers->count() > 0) { $this->generateName();
$this->name = str("{$this->servers->first()->name}-{$this->network}")->kebab();
}
} }
public function generate_name() public function updatedServerId()
{ {
$this->server = Server::find($this->server_id); $this->selectedServer = $this->servers->find($this->serverId);
$this->name = str("{$this->server->name}-{$this->network}")->kebab(); $this->generateName();
}
public function generateName()
{
$name = data_get($this->selectedServer, 'name', new Cuid2);
$this->name = str("{$name}-{$this->network}")->kebab();
} }
public function submit() public function submit()
{ {
$this->validate();
try { try {
$this->server = Server::find($this->server_id); $this->validate();
if ($this->is_swarm) { if ($this->isSwarm) {
$found = $this->server->swarmDockers()->where('network', $this->network)->first(); $found = $this->selectedServer->swarmDockers()->where('network', $this->network)->first();
if ($found) { if ($found) {
$this->dispatch('error', 'Network already added to this server.'); throw new \Exception('Network already added to this server.');
return;
} else { } else {
$docker = SwarmDocker::create([ $docker = SwarmDocker::create([
'name' => $this->name, 'name' => $this->name,
'network' => $this->network, 'network' => $this->network,
'server_id' => $this->server_id, 'server_id' => $this->selectedServer->id,
]); ]);
} }
} else { } else {
$found = $this->server->standaloneDockers()->where('network', $this->network)->first(); $found = $this->selectedServer->standaloneDockers()->where('network', $this->network)->first();
if ($found) { if ($found) {
$this->dispatch('error', 'Network already added to this server.'); throw new \Exception('Network already added to this server.');
return;
} else { } else {
$docker = ModelsStandaloneDocker::create([ $docker = StandaloneDocker::create([
'name' => $this->name, 'name' => $this->name,
'network' => $this->network, 'network' => $this->network,
'server_id' => $this->server_id, 'server_id' => $this->selectedServer->id,
]); ]);
} }
} }
$this->createNetworkAndAttachToProxy(); $connectProxyToDockerNetworks = connectProxyToNetworks($this->selectedServer);
instant_remote_process($connectProxyToDockerNetworks, $this->selectedServer, false);
return redirect()->route('destination.show', $docker->uuid); $this->dispatch('reloadWindow');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
} }
private function createNetworkAndAttachToProxy()
{
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
}
} }

View File

@@ -5,71 +5,91 @@ namespace App\Livewire\Destination;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use App\Models\SwarmDocker; use App\Models\SwarmDocker;
use Illuminate\Support\Collection; use Livewire\Attributes\Locked;
use Livewire\Attributes\Rule;
use Livewire\Component; use Livewire\Component;
class Show extends Component class Show extends Component
{ {
public Server $server; #[Locked]
public $destination;
public Collection|array $networks = []; #[Rule(['string', 'required'])]
public string $name;
private function createNetworkAndAttachToProxy() #[Rule(['string', 'required'])]
public string $network;
#[Rule(['string', 'required'])]
public string $serverIp;
public function mount(string $destination_uuid)
{ {
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server); try {
instant_remote_process($connectProxyToDockerNetworks, $this->server, false); $destination = StandaloneDocker::whereUuid($destination_uuid)->first() ??
} SwarmDocker::whereUuid($destination_uuid)->firstOrFail();
public function add($name) $ownedByTeam = Server::ownedByCurrentTeam()->each(function ($server) use ($destination) {
{ if ($server->standaloneDockers->contains($destination) || $server->swarmDockers->contains($destination)) {
if ($this->server->isSwarm()) { $this->destination = $destination;
$found = $this->server->swarmDockers()->where('network', $name)->first(); $this->syncData();
if ($found) {
$this->dispatch('error', 'Network already added to this server.');
return;
} else {
SwarmDocker::create([
'name' => $this->server->name.'-'.$name,
'network' => $this->name,
'server_id' => $this->server->id,
]);
} }
} else {
$found = $this->server->standaloneDockers()->where('network', $name)->first();
if ($found) {
$this->dispatch('error', 'Network already added to this server.');
return;
} else {
StandaloneDocker::create([
'name' => $this->server->name.'-'.$name,
'network' => $name,
'server_id' => $this->server->id,
]);
}
$this->createNetworkAndAttachToProxy();
}
}
public function scan()
{
if ($this->server->isSwarm()) {
$alreadyAddedNetworks = $this->server->swarmDockers;
} else {
$alreadyAddedNetworks = $this->server->standaloneDockers;
}
$networks = instant_remote_process(['docker network ls --format "{{json .}}"'], $this->server, false);
$this->networks = format_docker_command_output_to_json($networks)->filter(function ($network) {
return $network['Name'] !== 'bridge' && $network['Name'] !== 'host' && $network['Name'] !== 'none';
})->filter(function ($network) use ($alreadyAddedNetworks) {
return ! $alreadyAddedNetworks->contains('network', $network['Name']);
}); });
if ($this->networks->count() === 0) { if ($ownedByTeam === false) {
$this->dispatch('success', 'No new destinations found on this server.'); return redirect()->route('destination.index');
}
$this->destination = $destination;
$this->syncData();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
return; public function syncData(bool $toModel = false)
} {
$this->dispatch('success', 'Scan done.'); if ($toModel) {
$this->validate();
$this->destination->name = $this->name;
$this->destination->network = $this->network;
$this->destination->server->ip = $this->serverIp;
$this->destination->save();
} else {
$this->name = $this->destination->name;
$this->network = $this->destination->network;
$this->serverIp = $this->destination->server->ip;
}
}
public function submit()
{
try {
$this->syncData(true);
$this->dispatch('success', 'Destination saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function delete()
{
try {
if ($this->destination->getMorphClass() === \App\Models\StandaloneDocker::class) {
if ($this->destination->attachedTo()) {
return $this->dispatch('error', 'You must delete all resources before deleting this destination.');
}
instant_remote_process(["docker network disconnect {$this->destination->network} coolify-proxy"], $this->destination->server, throwError: false);
instant_remote_process(['docker network rm -f '.$this->destination->network], $this->destination->server);
}
$this->destination->delete();
return redirect()->route('destination.index');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.destination.show');
} }
} }

View File

@@ -5,55 +5,39 @@ namespace App\Livewire;
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\Http;
use Illuminate\Support\Facades\Route; use Livewire\Attributes\Rule;
use Livewire\Component; use Livewire\Component;
class Help extends Component class Help extends Component
{ {
use WithRateLimiting; use WithRateLimiting;
#[Rule(['required', 'min:10', 'max:1000'])]
public string $description; public string $description;
#[Rule(['required', 'min:3'])]
public string $subject; public string $subject;
public ?string $path = null;
protected $rules = [
'description' => 'required|min:10',
'subject' => 'required|min:3',
];
public function mount()
{
$this->path = Route::current()?->uri() ?? null;
if (isDev()) {
$this->description = "I'm having trouble with {$this->path}";
$this->subject = "Help with {$this->path}";
}
}
public function submit() public function submit()
{ {
try { try {
$this->rateLimit(3, 30);
$this->validate(); $this->validate();
$debug = "Route: {$this->path}"; $this->rateLimit(3, 30);
$settings = instanceSettings();
$mail = new MailMessage; $mail = new MailMessage;
$mail->view( $mail->view(
'emails.help', 'emails.help',
[ [
'description' => $this->description, 'description' => $this->description,
'debug' => $debug,
] ]
); );
$mail->subject("[HELP]: {$this->subject}"); $mail->subject("[HELP]: {$this->subject}");
$settings = instanceSettings();
$type = set_transanctional_email_settings($settings); $type = set_transanctional_email_settings($settings);
if (! $type) {
// Sending feedback through Cloud API
if ($type === false) {
$url = 'https://app.coolify.io/api/feedback'; $url = 'https://app.coolify.io/api/feedback';
if (isDev()) {
$url = 'http://localhost:80/api/feedback';
}
Http::post($url, [ Http::post($url, [
'content' => 'User: `'.auth()->user()?->email.'` with subject: `'.$this->subject.'` has the following problem: `'.$this->description.'`', 'content' => 'User: `'.auth()->user()?->email.'` with subject: `'.$this->subject.'` has the following problem: `'.$this->description.'`',
]); ]);

View File

@@ -4,47 +4,94 @@ namespace App\Livewire\Notifications;
use App\Models\Team; use App\Models\Team;
use App\Notifications\Test; use App\Notifications\Test;
use Livewire\Attributes\Rule;
use Livewire\Component; use Livewire\Component;
class Discord extends Component class Discord extends Component
{ {
public Team $team; public Team $team;
protected $rules = [ #[Rule(['boolean'])]
'team.discord_enabled' => 'nullable|boolean', public bool $discordEnabled = false;
'team.discord_webhook_url' => 'required|url',
'team.discord_notifications_test' => 'nullable|boolean',
'team.discord_notifications_deployments' => 'nullable|boolean',
'team.discord_notifications_status_changes' => 'nullable|boolean',
'team.discord_notifications_database_backups' => 'nullable|boolean',
'team.discord_notifications_scheduled_tasks' => 'nullable|boolean',
'team.discord_notifications_server_disk_usage' => 'nullable|boolean',
];
protected $validationAttributes = [ #[Rule(['url', 'nullable'])]
'team.discord_webhook_url' => 'Discord Webhook', public ?string $discordWebhookUrl = null;
];
#[Rule(['boolean'])]
public bool $discordNotificationsTest = false;
#[Rule(['boolean'])]
public bool $discordNotificationsDeployments = false;
#[Rule(['boolean'])]
public bool $discordNotificationsStatusChanges = false;
#[Rule(['boolean'])]
public bool $discordNotificationsDatabaseBackups = false;
#[Rule(['boolean'])]
public bool $discordNotificationsScheduledTasks = false;
#[Rule(['boolean'])]
public bool $discordNotificationsServerDiskUsage = false;
public function mount() public function mount()
{ {
try {
$this->team = auth()->user()->currentTeam(); $this->team = auth()->user()->currentTeam();
$this->syncData();
} catch (\Throwable $e) {
handleError($e, $this);
}
}
public function syncData(bool $toModel = false)
{
if ($toModel) {
$this->validate();
$this->team->discord_enabled = $this->discordEnabled;
$this->team->discord_webhook_url = $this->discordWebhookUrl;
$this->team->discord_notifications_test = $this->discordNotificationsTest;
$this->team->discord_notifications_deployments = $this->discordNotificationsDeployments;
$this->team->discord_notifications_status_changes = $this->discordNotificationsStatusChanges;
$this->team->discord_notifications_database_backups = $this->discordNotificationsDatabaseBackups;
$this->team->discord_notifications_scheduled_tasks = $this->discordNotificationsScheduledTasks;
$this->team->discord_notifications_server_disk_usage = $this->discordNotificationsServerDiskUsage;
try {
$this->saveModel();
} catch (\Throwable $e) {
return handleError($e, $this);
}
} else {
$this->discordEnabled = $this->team->discord_enabled;
$this->discordWebhookUrl = $this->team->discord_webhook_url;
$this->discordNotificationsTest = $this->team->discord_notifications_test;
$this->discordNotificationsDeployments = $this->team->discord_notifications_deployments;
$this->discordNotificationsStatusChanges = $this->team->discord_notifications_status_changes;
$this->discordNotificationsDatabaseBackups = $this->team->discord_notifications_database_backups;
$this->discordNotificationsScheduledTasks = $this->team->discord_notifications_scheduled_tasks;
$this->discordNotificationsServerDiskUsage = $this->team->discord_notifications_server_disk_usage;
}
} }
public function instantSave() public function instantSave()
{ {
try { try {
$this->submit(); $this->syncData(true);
} catch (\Throwable) { } catch (\Throwable $e) {
$this->team->discord_enabled = false; return handleError($e, $this);
$this->validate();
} }
} }
public function submit() public function submit()
{ {
try {
$this->resetErrorBag(); $this->resetErrorBag();
$this->validate(); $this->syncData(true);
$this->saveModel(); $this->saveModel();
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function saveModel() public function saveModel()
@@ -56,8 +103,12 @@ class Discord extends Component
public function sendTestNotification() public function sendTestNotification()
{ {
$this->team?->notify(new Test); try {
$this->team->notify(new Test);
$this->dispatch('success', 'Test notification sent.'); $this->dispatch('success', 'Test notification sent.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function render() public function render()

View File

@@ -3,24 +3,17 @@
namespace App\Livewire\Project; namespace App\Livewire\Project;
use App\Models\Project; use App\Models\Project;
use Livewire\Attributes\Rule;
use Livewire\Component; use Livewire\Component;
class AddEmpty extends Component class AddEmpty extends Component
{ {
public string $name = ''; #[Rule(['required', 'string', 'min:3'])]
public string $name;
#[Rule(['nullable', 'string'])]
public string $description = ''; public string $description = '';
protected $rules = [
'name' => 'required|string|min:3',
'description' => 'nullable|string',
];
protected $validationAttributes = [
'name' => 'Project Name',
'description' => 'Project Description',
];
public function submit() public function submit()
{ {
try { try {
@@ -34,8 +27,6 @@ class AddEmpty extends Component
return redirect()->route('project.show', $project->uuid); return redirect()->route('project.show', $project->uuid);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} finally {
$this->name = '';
} }
} }
} }

View File

@@ -1,44 +0,0 @@
<?php
namespace App\Livewire\Project;
use App\Models\Environment;
use App\Models\Project;
use Livewire\Component;
class AddEnvironment extends Component
{
public Project $project;
public string $name = '';
public string $description = '';
protected $rules = [
'name' => 'required|string|min:3',
];
protected $validationAttributes = [
'name' => 'Environment Name',
];
public function submit()
{
try {
$this->validate();
$environment = Environment::create([
'name' => $this->name,
'project_id' => $this->project->id,
]);
return redirect()->route('project.resource.index', [
'project_uuid' => $this->project->uuid,
'environment_name' => $environment->name,
]);
} catch (\Throwable $e) {
handleError($e, $this);
} finally {
$this->name = '';
}
}
}

View File

@@ -4,55 +4,92 @@ namespace App\Livewire\Project\Application;
use App\Models\Application; use App\Models\Application;
use App\Models\PrivateKey; use App\Models\PrivateKey;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Rule;
use Livewire\Component; use Livewire\Component;
class Source extends Component class Source extends Component
{ {
public $applicationId;
public Application $application; public Application $application;
public $private_keys; #[Locked]
public $privateKeys;
protected $rules = [ #[Rule(['nullable', 'string'])]
'application.git_repository' => 'required', public ?string $privateKeyName = null;
'application.git_branch' => 'required',
'application.git_commit_sha' => 'nullable',
];
protected $validationAttributes = [ #[Rule(['nullable', 'integer'])]
'application.git_repository' => 'repository', public ?int $privateKeyId = null;
'application.git_branch' => 'branch',
'application.git_commit_sha' => 'commit sha', #[Rule(['required', 'string'])]
]; public string $gitRepository;
#[Rule(['required', 'string'])]
public string $gitBranch;
#[Rule(['nullable', 'string'])]
public ?string $gitCommitSha = null;
public function mount() public function mount()
{ {
$this->get_private_keys(); try {
$this->syncData();
$this->getPrivateKeys();
} catch (\Throwable $e) {
handleError($e, $this);
}
} }
private function get_private_keys() public function syncData(bool $toModel = false)
{ {
$this->private_keys = PrivateKey::whereTeamId(currentTeam()->id)->get()->reject(function ($key) { if ($toModel) {
return $key->id == $this->application->private_key_id; $this->validate();
$this->application->update([
'git_repository' => $this->gitRepository,
'git_branch' => $this->gitBranch,
'git_commit_sha' => $this->gitCommitSha,
'private_key_id' => $this->privateKeyId,
]);
} else {
$this->gitRepository = $this->application->git_repository;
$this->gitBranch = $this->application->git_branch;
$this->gitCommitSha = $this->application->git_commit_sha;
$this->privateKeyId = $this->application->private_key_id;
$this->privateKeyName = data_get($this->application, 'private_key.name');
}
}
private function getPrivateKeys()
{
$this->privateKeys = PrivateKey::whereTeamId(currentTeam()->id)->get()->reject(function ($key) {
return $key->id == $this->privateKeyId;
}); });
} }
public function setPrivateKey(int $private_key_id) public function setPrivateKey(int $privateKeyId)
{ {
$this->application->private_key_id = $private_key_id; try {
$this->application->save(); $this->privateKeyId = $privateKeyId;
$this->syncData(true);
$this->getPrivateKeys();
$this->application->refresh(); $this->application->refresh();
$this->get_private_keys(); $this->privateKeyName = $this->application->private_key->name;
$this->dispatch('success', 'Private key updated!');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function submit() public function submit()
{ {
$this->validate(); try {
if (! $this->application->git_commit_sha) { if (str($this->gitCommitSha)->isEmpty()) {
$this->application->git_commit_sha = 'HEAD'; $this->gitCommitSha = 'HEAD';
} }
$this->application->save(); $this->syncData(true);
$this->dispatch('success', 'Application source updated!'); $this->dispatch('success', 'Application source updated!');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
} }

View File

@@ -3,34 +3,47 @@
namespace App\Livewire\Project; namespace App\Livewire\Project;
use App\Models\Project; use App\Models\Project;
use Livewire\Attributes\Rule;
use Livewire\Component; use Livewire\Component;
class Edit extends Component class Edit extends Component
{ {
public Project $project; public Project $project;
protected $rules = [ #[Rule(['required', 'string', 'min:3', 'max:255'])]
'project.name' => 'required|min:3|max:255', public string $name;
'project.description' => 'nullable|string|max:255',
];
public function mount() #[Rule(['nullable', 'string', 'max:255'])]
public ?string $description = null;
public function mount(string $project_uuid)
{ {
$projectUuid = request()->route('project_uuid'); try {
$teamId = currentTeam()->id; $this->project = Project::where('team_id', currentTeam()->id)->where('uuid', $project_uuid)->firstOrFail();
$project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first(); $this->syncData();
if (! $project) { } catch (\Throwable $e) {
return redirect()->route('dashboard'); return handleError($e, $this);
}
}
public function syncData(bool $toModel = false)
{
if ($toModel) {
$this->validate();
$this->project->update([
'name' => $this->name,
'description' => $this->description,
]);
} else {
$this->name = $this->project->name;
$this->description = $this->project->description;
} }
$this->project = $project;
} }
public function submit() public function submit()
{ {
try { try {
$this->validate(); $this->syncData(true);
$this->project->save();
$this->dispatch('saved');
$this->dispatch('success', 'Project updated.'); $this->dispatch('success', 'Project updated.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);

View File

@@ -4,6 +4,8 @@ namespace App\Livewire\Project;
use App\Models\Application; use App\Models\Application;
use App\Models\Project; use App\Models\Project;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Rule;
use Livewire\Component; use Livewire\Component;
class EnvironmentEdit extends Component class EnvironmentEdit extends Component
@@ -12,29 +14,45 @@ class EnvironmentEdit extends Component
public Application $application; public Application $application;
#[Locked]
public $environment; public $environment;
public array $parameters; #[Rule(['required', 'string', 'min:3', 'max:255'])]
public string $name;
protected $rules = [ #[Rule(['nullable', 'string', 'max:255'])]
'environment.name' => 'required|min:3|max:255', public ?string $description = null;
'environment.description' => 'nullable|min:3|max:255',
];
public function mount() public function mount(string $project_uuid, string $environment_name)
{ {
$this->parameters = get_route_parameters(); try {
$this->project = Project::ownedByCurrentTeam()->where('uuid', request()->route('project_uuid'))->first(); $this->project = Project::ownedByCurrentTeam()->where('uuid', $project_uuid)->firstOrFail();
$this->environment = $this->project->environments()->where('name', request()->route('environment_name'))->first(); $this->environment = $this->project->environments()->where('name', $environment_name)->firstOrFail();
$this->syncData();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function syncData(bool $toModel = false)
{
if ($toModel) {
$this->validate();
$this->environment->update([
'name' => $this->name,
'description' => $this->description,
]);
} else {
$this->name = $this->environment->name;
$this->description = $this->environment->description;
}
} }
public function submit() public function submit()
{ {
$this->validate();
try { try {
$this->environment->save(); $this->syncData(true);
$this->redirectRoute('project.environment.edit', ['environment_name' => $this->environment->name, 'project_uuid' => $this->project->uuid]);
return redirect()->route('project.environment.edit', ['project_uuid' => $this->project->uuid, 'environment_name' => $this->environment->name]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -2,27 +2,46 @@
namespace App\Livewire\Project; namespace App\Livewire\Project;
use App\Models\Environment;
use App\Models\Project; use App\Models\Project;
use Livewire\Attributes\Rule;
use Livewire\Component; use Livewire\Component;
class Show extends Component class Show extends Component
{ {
public Project $project; public Project $project;
public $environments; #[Rule(['required', 'string', 'min:3'])]
public string $name;
public function mount() #[Rule(['nullable', 'string'])]
public ?string $description = null;
public function mount(string $project_uuid)
{ {
$projectUuid = request()->route('project_uuid'); try {
$teamId = currentTeam()->id; $this->project = Project::where('team_id', currentTeam()->id)->where('uuid', $project_uuid)->firstOrFail();
} catch (\Throwable $e) {
$project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first(); return handleError($e, $this);
if (! $project) { }
return redirect()->route('dashboard');
} }
$this->environments = $project->environments->sortBy('created_at'); public function submit()
$this->project = $project; {
try {
$this->validate();
$environment = Environment::create([
'name' => $this->name,
'project_id' => $this->project->id,
]);
return redirect()->route('project.resource.index', [
'project_uuid' => $this->project->uuid,
'environment_name' => $environment->name,
]);
} catch (\Throwable $e) {
handleError($e, $this);
}
} }
public function render() public function render()

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace App\Livewire\Server\Destination; namespace App\Livewire\Server;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
@@ -8,7 +8,7 @@ use App\Models\SwarmDocker;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
class Show extends Component class Destinations extends Component
{ {
public Server $server; public Server $server;
@@ -86,6 +86,6 @@ class Show extends Component
public function render() public function render()
{ {
return view('livewire.server.destination.show'); return view('livewire.server.destinations');
} }
} }

View File

@@ -3,102 +3,83 @@
namespace App\Livewire; namespace App\Livewire;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use Livewire\Attributes\Rule;
use Livewire\Component; use Livewire\Component;
class SettingsEmail extends Component class SettingsEmail extends Component
{ {
public InstanceSettings $settings; public InstanceSettings $settings;
public string $emails; #[Rule(['boolean'])]
public bool $smtpEnabled = false;
protected $rules = [ #[Rule(['nullable', 'string'])]
'settings.smtp_enabled' => 'nullable|boolean', public ?string $smtpHost = null;
'settings.smtp_host' => 'required',
'settings.smtp_port' => 'required|numeric',
'settings.smtp_encryption' => 'nullable',
'settings.smtp_username' => 'nullable',
'settings.smtp_password' => 'nullable',
'settings.smtp_timeout' => 'nullable',
'settings.smtp_from_address' => 'required|email',
'settings.smtp_from_name' => 'required',
'settings.resend_enabled' => 'nullable|boolean',
'settings.resend_api_key' => 'nullable',
]; #[Rule(['nullable', 'numeric', 'min:1', 'max:65535'])]
public ?int $smtpPort = null;
protected $validationAttributes = [ #[Rule(['nullable', 'string'])]
'settings.smtp_from_address' => 'From Address', public ?string $smtpEncryption = null;
'settings.smtp_from_name' => 'From Name',
'settings.smtp_recipients' => 'Recipients', #[Rule(['nullable', 'string'])]
'settings.smtp_host' => 'Host', public ?string $smtpUsername = null;
'settings.smtp_port' => 'Port',
'settings.smtp_encryption' => 'Encryption', #[Rule(['nullable'])]
'settings.smtp_username' => 'Username', public ?string $smtpPassword = null;
'settings.smtp_password' => 'Password',
'settings.smtp_timeout' => 'Timeout', #[Rule(['nullable', 'numeric'])]
'settings.resend_api_key' => 'Resend API Key', public ?int $smtpTimeout = null;
];
#[Rule(['nullable', 'email'])]
public ?string $smtpFromAddress = null;
#[Rule(['nullable', 'string'])]
public ?string $smtpFromName = null;
#[Rule(['boolean'])]
public bool $resendEnabled = false;
#[Rule(['nullable', 'string'])]
public ?string $resendApiKey = null;
public function mount() public function mount()
{ {
if (isInstanceAdmin()) { if (isInstanceAdmin() === false) {
$this->settings = instanceSettings();
$this->emails = auth()->user()->email;
} else {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
$this->settings = instanceSettings();
$this->syncData();
} }
public function submitFromFields() public function syncData(bool $toModel = false)
{ {
try { if ($toModel) {
$this->resetErrorBag(); $this->validate();
$this->validate([ $this->settings->smtp_enabled = $this->smtpEnabled;
'settings.smtp_from_address' => 'required|email', $this->settings->smtp_host = $this->smtpHost;
'settings.smtp_from_name' => 'required', $this->settings->smtp_port = $this->smtpPort;
]); $this->settings->smtp_encryption = $this->smtpEncryption;
$this->settings->smtp_username = $this->smtpUsername;
$this->settings->smtp_password = $this->smtpPassword;
$this->settings->smtp_timeout = $this->smtpTimeout;
$this->settings->resend_enabled = $this->resendEnabled;
$this->settings->resend_api_key = $this->resendApiKey;
$this->settings->save(); $this->settings->save();
$this->dispatch('success', 'Settings saved.'); } else {
} catch (\Throwable $e) { $this->smtpEnabled = $this->settings->smtp_enabled;
return handleError($e, $this); $this->smtpHost = $this->settings->smtp_host;
} $this->smtpPort = $this->settings->smtp_port;
} $this->smtpEncryption = $this->settings->smtp_encryption;
$this->smtpUsername = $this->settings->smtp_username;
$this->smtpPassword = $this->settings->smtp_password;
$this->smtpTimeout = $this->settings->smtp_timeout;
$this->smtpFromAddress = $this->settings->smtp_from_address;
$this->smtpFromName = $this->settings->smtp_from_name;
public function submitResend() $this->resendEnabled = $this->settings->resend_enabled;
{ $this->resendApiKey = $this->settings->resend_api_key;
try {
$this->resetErrorBag();
$this->validate([
'settings.smtp_from_address' => 'required|email',
'settings.smtp_from_name' => 'required',
'settings.resend_api_key' => 'required',
]);
$this->settings->save();
$this->dispatch('success', 'Settings saved.');
} catch (\Throwable $e) {
$this->settings->resend_enabled = false;
return handleError($e, $this);
}
}
public function instantSaveResend()
{
try {
$this->settings->smtp_enabled = false;
$this->submitResend();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {
$this->settings->resend_enabled = false;
$this->submit();
} catch (\Throwable $e) {
return handleError($e, $this);
} }
} }
@@ -106,20 +87,29 @@ class SettingsEmail extends Component
{ {
try { try {
$this->resetErrorBag(); $this->resetErrorBag();
$this->validate([ $this->syncData(true);
'settings.smtp_from_address' => 'required|email',
'settings.smtp_from_name' => 'required',
'settings.smtp_host' => 'required',
'settings.smtp_port' => 'required|numeric',
'settings.smtp_encryption' => 'nullable',
'settings.smtp_username' => 'nullable',
'settings.smtp_password' => 'nullable',
'settings.smtp_timeout' => 'nullable',
]);
$this->settings->save();
$this->dispatch('success', 'Settings saved.'); $this->dispatch('success', 'Settings saved.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function instantSave(string $type)
{
try {
if ($type === 'SMTP') {
$this->resendEnabled = false;
} else {
$this->smtpEnabled = false;
}
$this->syncData(true);
if ($this->smtpEnabled || $this->resendEnabled) {
$this->dispatch('success', "{$type} enabled.");
} else {
$this->dispatch('success', "{$type} disabled.");
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
} }

View File

@@ -74,6 +74,9 @@ class AdminView extends Component
public function delete($id, $password) public function delete($id, $password)
{ {
if (! isInstanceAdmin()) {
return redirect()->route('dashboard');
}
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
if (! Hash::check($password, Auth::user()->password)) { if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.'); $this->addError('password', 'The provided password is incorrect.');

View File

@@ -117,14 +117,31 @@ class Application extends BaseModel
if ($application->fqdn === '') { if ($application->fqdn === '') {
$application->fqdn = null; $application->fqdn = null;
} }
$application->forceFill([ $payload = [];
'fqdn' => $application->fqdn, if ($application->isDirty('fqdn')) {
'install_command' => str($application->install_command)->trim(), $payload['fqdn'] = $application->fqdn;
'build_command' => str($application->build_command)->trim(), }
'start_command' => str($application->start_command)->trim(), if ($application->isDirty('install_command')) {
'base_directory' => str($application->base_directory)->trim(), $payload['install_command'] = str($application->install_command)->trim();
'publish_directory' => str($application->publish_directory)->trim(), }
]); if ($application->isDirty('build_command')) {
$payload['build_command'] = str($application->build_command)->trim();
}
if ($application->isDirty('start_command')) {
$payload['start_command'] = str($application->start_command)->trim();
}
if ($application->isDirty('base_directory')) {
$payload['base_directory'] = str($application->base_directory)->trim();
}
if ($application->isDirty('publish_directory')) {
$payload['publish_directory'] = str($application->publish_directory)->trim();
}
if ($application->isDirty('status')) {
$payload['last_online_at'] = now();
}
if (count($payload) > 0) {
$application->forceFill($payload);
}
}); });
static::created(function ($application) { static::created(function ($application) {
ApplicationSetting::create([ ApplicationSetting::create([

View File

@@ -28,6 +28,11 @@ class ApplicationPreview extends BaseModel
}); });
} }
}); });
static::saving(function ($preview) {
if ($preview->isDirty('status')) {
$preview->forceFill(['last_online_at' => now()]);
}
});
} }
public static function findPreviewByApplicationAndPullId(int $application_id, int $pull_request_id) public static function findPreviewByApplicationAndPullId(int $application_id, int $pull_request_id)

View File

@@ -507,20 +507,6 @@ $schema://$host {
return Server::whereTeamId($teamId)->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_build_server', true); return Server::whereTeamId($teamId)->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_build_server', true);
} }
public function skipServer()
{
if ($this->ip === '1.2.3.4') {
// ray('skipping 1.2.3.4');
return true;
}
if ($this->settings->force_disabled === true) {
// ray('force_disabled');
return true;
}
return false;
}
public function isForceDisabled() public function isForceDisabled()
{ {
return $this->settings->force_disabled; return $this->settings->force_disabled;
@@ -691,7 +677,7 @@ $schema://$host {
} }
} }
} else { } else {
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this, false); $containers = instant_remote_process(["docker container inspect $(docker container ls -aq) --format '{{json .}}'"], $this, false);
$containers = format_docker_command_output_to_json($containers); $containers = format_docker_command_output_to_json($containers);
$containerReplicates = collect([]); $containerReplicates = collect([]);
} }
@@ -917,11 +903,23 @@ $schema://$host {
return true; return true;
} }
public function skipServer()
{
if ($this->ip === '1.2.3.4') {
return true;
}
if ($this->settings->force_disabled === true) {
return true;
}
return false;
}
public function isFunctional() public function isFunctional()
{ {
$isFunctional = $this->settings->is_reachable && $this->settings->is_usable && ! $this->settings->force_disabled; $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && $this->settings->force_disabled === false && $this->ip !== '1.2.3.4';
if (! $isFunctional) { if ($isFunctional === false) {
Storage::disk('ssh-mux')->delete($this->muxFilename()); Storage::disk('ssh-mux')->delete($this->muxFilename());
} }
@@ -976,10 +974,10 @@ $schema://$host {
public function serverStatus(): bool public function serverStatus(): bool
{ {
if ($this->status() === false) { if ($this->isFunctional() === false) {
return false; return false;
} }
if ($this->isFunctional() === false) { if ($this->status() === false) {
return false; return false;
} }
@@ -988,7 +986,7 @@ $schema://$host {
public function status(): bool public function status(): bool
{ {
if ($this->skipServer()) { if ($this->isFunctional() === false) {
return false; return false;
} }
['uptime' => $uptime] = $this->validateConnection(false); ['uptime' => $uptime] = $this->validateConnection(false);

View File

@@ -19,6 +19,11 @@ class ServiceApplication extends BaseModel
$service->persistentStorages()->delete(); $service->persistentStorages()->delete();
$service->fileStorages()->delete(); $service->fileStorages()->delete();
}); });
static::saving(function ($service) {
if ($service->isDirty('status')) {
$service->forceFill(['last_online_at' => now()]);
}
});
} }
public function restart() public function restart()

View File

@@ -17,6 +17,11 @@ class ServiceDatabase extends BaseModel
$service->persistentStorages()->delete(); $service->persistentStorages()->delete();
$service->fileStorages()->delete(); $service->fileStorages()->delete();
}); });
static::saving(function ($service) {
if ($service->isDirty('status')) {
$service->forceFill(['last_online_at' => now()]);
}
});
} }
public function restart() public function restart()

View File

@@ -38,6 +38,11 @@ class StandaloneClickhouse extends BaseModel
$database->environment_variables()->delete(); $database->environment_variables()->delete();
$database->tags()->detach(); $database->tags()->detach();
}); });
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
}
});
} }
protected function serverStatus(): Attribute protected function serverStatus(): Attribute

View File

@@ -38,6 +38,11 @@ class StandaloneDragonfly extends BaseModel
$database->environment_variables()->delete(); $database->environment_variables()->delete();
$database->tags()->detach(); $database->tags()->detach();
}); });
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
}
});
} }
protected function serverStatus(): Attribute protected function serverStatus(): Attribute

View File

@@ -38,6 +38,11 @@ class StandaloneKeydb extends BaseModel
$database->environment_variables()->delete(); $database->environment_variables()->delete();
$database->tags()->detach(); $database->tags()->detach();
}); });
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
}
});
} }
protected function serverStatus(): Attribute protected function serverStatus(): Attribute

View File

@@ -38,6 +38,11 @@ class StandaloneMariadb extends BaseModel
$database->environment_variables()->delete(); $database->environment_variables()->delete();
$database->tags()->detach(); $database->tags()->detach();
}); });
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
}
});
} }
protected function serverStatus(): Attribute protected function serverStatus(): Attribute

View File

@@ -42,6 +42,11 @@ class StandaloneMongodb extends BaseModel
$database->environment_variables()->delete(); $database->environment_variables()->delete();
$database->tags()->detach(); $database->tags()->detach();
}); });
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
}
});
} }
protected function serverStatus(): Attribute protected function serverStatus(): Attribute

View File

@@ -39,6 +39,11 @@ class StandaloneMysql extends BaseModel
$database->environment_variables()->delete(); $database->environment_variables()->delete();
$database->tags()->detach(); $database->tags()->detach();
}); });
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
}
});
} }
protected function serverStatus(): Attribute protected function serverStatus(): Attribute

View File

@@ -39,6 +39,11 @@ class StandalonePostgresql extends BaseModel
$database->environment_variables()->delete(); $database->environment_variables()->delete();
$database->tags()->detach(); $database->tags()->detach();
}); });
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
}
});
} }
public function workdir() public function workdir()

View File

@@ -34,6 +34,11 @@ class StandaloneRedis extends BaseModel
$database->environment_variables()->delete(); $database->environment_variables()->delete();
$database->tags()->detach(); $database->tags()->detach();
}); });
static::saving(function ($database) {
if ($database->isDirty('status')) {
$database->forceFill(['last_online_at' => now()]);
}
});
} }
protected function serverStatus(): Attribute protected function serverStatus(): Attribute

View File

@@ -75,7 +75,8 @@ class FortifyServiceProvider extends ServiceProvider
}); });
Fortify::authenticateUsing(function (Request $request) { Fortify::authenticateUsing(function (Request $request) {
$user = User::where('email', $request->email)->with('teams')->first(); $email = strtolower($request->email);
$user = User::where('email', $email)->with('teams')->first();
if ( if (
$user && $user &&
Hash::check($request->password, $user->password) Hash::check($request->password, $user->password)

View File

@@ -23,6 +23,8 @@ class Input extends Component
public bool $isMultiline = false, public bool $isMultiline = false,
public string $defaultClass = 'input', public string $defaultClass = 'input',
public string $autocomplete = 'off', public string $autocomplete = 'off',
public ?int $minlength = null,
public ?int $maxlength = null,
) {} ) {}
public function render(): View|Closure|string public function render(): View|Closure|string

View File

@@ -30,7 +30,9 @@ class Textarea extends Component
public bool $realtimeValidation = false, public bool $realtimeValidation = false,
public bool $allowToPeak = true, public bool $allowToPeak = true,
public string $defaultClass = 'input scrollbar font-mono', public string $defaultClass = 'input scrollbar font-mono',
public string $defaultClassInput = 'input' public string $defaultClassInput = 'input',
public ?int $minlength = null,
public ?int $maxlength = null,
) { ) {
// //
} }

View File

@@ -197,6 +197,7 @@ return [
'production' => [ 'production' => [
's6' => [ 's6' => [
'autoScalingStrategy' => 'size', 'autoScalingStrategy' => 'size',
'minProcesses' => env('HORIZON_MIN_PROCESSES', 1),
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6), 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6),
'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1), 'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1), 'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),
@@ -206,6 +207,7 @@ return [
'local' => [ 'local' => [
's6' => [ 's6' => [
'autoScalingStrategy' => 'size', 'autoScalingStrategy' => 'size',
'minProcesses' => env('HORIZON_MIN_PROCESSES', 1),
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6), 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6),
'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1), 'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1), 'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),

View File

@@ -76,8 +76,8 @@ return [
*/ */
'queue' => [ 'queue' => [
'connection' => env('TELESCOPE_QUEUE_CONNECTION', null), 'connection' => env('TELESCOPE_QUEUE_CONNECTION', 'redis'),
'queue' => env('TELESCOPE_QUEUE', null), 'queue' => env('TELESCOPE_QUEUE', 'default'),
], ],
/* /*

View File

@@ -0,0 +1,96 @@
<?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('applications', function (Blueprint $table) {
$table->timestamp('last_online_at')->default(now())->after('updated_at');
});
Schema::table('application_previews', function (Blueprint $table) {
$table->timestamp('last_online_at')->default(now())->after('updated_at');
});
Schema::table('service_applications', function (Blueprint $table) {
$table->timestamp('last_online_at')->default(now())->after('updated_at');
});
Schema::table('service_databases', function (Blueprint $table) {
$table->timestamp('last_online_at')->default(now())->after('updated_at');
});
Schema::table('standalone_postgresqls', function (Blueprint $table) {
$table->timestamp('last_online_at')->default(now())->after('updated_at');
});
Schema::table('standalone_redis', function (Blueprint $table) {
$table->timestamp('last_online_at')->default(now())->after('updated_at');
});
Schema::table('standalone_mongodbs', function (Blueprint $table) {
$table->timestamp('last_online_at')->default(now())->after('updated_at');
});
Schema::table('standalone_mysqls', function (Blueprint $table) {
$table->timestamp('last_online_at')->default(now())->after('updated_at');
});
Schema::table('standalone_mariadbs', function (Blueprint $table) {
$table->timestamp('last_online_at')->default(now())->after('updated_at');
});
Schema::table('standalone_keydbs', function (Blueprint $table) {
$table->timestamp('last_online_at')->default(now())->after('updated_at');
});
Schema::table('standalone_dragonflies', function (Blueprint $table) {
$table->timestamp('last_online_at')->default(now())->after('updated_at');
});
Schema::table('standalone_clickhouses', function (Blueprint $table) {
$table->timestamp('last_online_at')->default(now())->after('updated_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('last_online_at');
});
Schema::table('application_previews', function (Blueprint $table) {
$table->dropColumn('last_online_at');
});
Schema::table('service_applications', function (Blueprint $table) {
$table->dropColumn('last_online_at');
});
Schema::table('service_databases', function (Blueprint $table) {
$table->dropColumn('last_online_at');
});
Schema::table('standalone_postgresqls', function (Blueprint $table) {
$table->dropColumn('last_online_at');
});
Schema::table('standalone_redis', function (Blueprint $table) {
$table->dropColumn('last_online_at');
});
Schema::table('standalone_mongodbs', function (Blueprint $table) {
$table->dropColumn('last_online_at');
});
Schema::table('standalone_mysqls', function (Blueprint $table) {
$table->dropColumn('last_online_at');
});
Schema::table('standalone_mariadbs', function (Blueprint $table) {
$table->dropColumn('last_online_at');
});
Schema::table('standalone_keydbs', function (Blueprint $table) {
$table->dropColumn('last_online_at');
});
Schema::table('standalone_dragonflies', function (Blueprint $table) {
$table->dropColumn('last_online_at');
});
Schema::table('standalone_clickhouses', function (Blueprint $table) {
$table->dropColumn('last_online_at');
});
}
};

View File

@@ -41,8 +41,9 @@
@if ($id !== 'null') wire:model={{ $id }} @endif @if ($id !== 'null') wire:model={{ $id }} @endif
wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300' wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300'
wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled"
type="{{ $type }}" @disabled($disabled) type="{{ $type }}" @disabled($disabled) min="{{ $attributes->get('min') }}"
min="{{ $attributes->get('min') }}" max="{{ $attributes->get('max') }}" max="{{ $attributes->get('max') }}" minlength="{{ $attributes->get('minlength') }}"
maxlength="{{ $attributes->get('maxlength') }}"
@if ($id !== 'null') id={{ $id }} @endif name="{{ $name }}" @if ($id !== 'null') id={{ $id }} @endif name="{{ $name }}"
placeholder="{{ $attributes->get('placeholder') }}"> placeholder="{{ $attributes->get('placeholder') }}">
@endif @endif

View File

@@ -51,8 +51,8 @@
type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $id }}" type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $id }}"
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}" name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
aria-placeholder="{{ $attributes->get('placeholder') }}"> aria-placeholder="{{ $attributes->get('placeholder') }}">
<textarea x-cloak x-show="type !== 'password'" placeholder="{{ $placeholder }}" <textarea minlength="{{ $minlength }}" maxlength="{{ $maxlength }}" x-cloak x-show="type !== 'password'"
{{ $attributes->merge(['class' => $defaultClass]) }} placeholder="{{ $placeholder }}" {{ $attributes->merge(['class' => $defaultClass]) }}
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $id }}" @if ($realtimeValidation) wire:model.debounce.200ms="{{ $id }}"
@else @else
wire:model={{ $value ?? $id }} wire:model={{ $value ?? $id }}
@@ -62,7 +62,8 @@
</div> </div>
@else @else
<textarea {{ $allowTab ? '@keydown.tab=handleKeydown' : '' }} placeholder="{{ $placeholder }}" <textarea minlength="{{ $minlength }}" maxlength="{{ $maxlength }}"
{{ $allowTab ? '@keydown.tab=handleKeydown' : '' }} placeholder="{{ $placeholder }}"
{{ !$spellcheck ? 'spellcheck=false' : '' }} {{ $attributes->merge(['class' => $defaultClass]) }} {{ !$spellcheck ? 'spellcheck=false' : '' }} {{ $attributes->merge(['class' => $defaultClass]) }}
@if ($realtimeValidation) wire:model.debounce.200ms="{{ $id }}" @if ($realtimeValidation) wire:model.debounce.200ms="{{ $id }}"
@else @else

View File

@@ -148,7 +148,7 @@
<li> <li>
<a title="Destinations" <a title="Destinations"
class="{{ request()->is('destination*') ? 'menu-item-active menu-item' : 'menu-item' }}" class="{{ request()->is('destination*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('destination.all') }}"> href="{{ route('destination.index') }}" wire:navigate>
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" <path fill="none" stroke="currentColor" stroke-linecap="round"

View File

@@ -1,44 +0,0 @@
<x-layout>
<x-slot:title>
Destinations | Coolify
</x-slot>
<div class="flex items-start gap-2">
<h1>Destinations</h1>
@if ($servers->count() > 0)
<x-modal-input buttonTitle="+ Add" title="New Destination">
<livewire:destination.new.docker :server_id="$server_id" />
</x-modal-input>
@endif
</div>
<div class="subtitle">Network endpoints to deploy your resources.</div>
<div class="grid gap-2 lg:grid-cols-1">
@forelse ($destinations as $destination)
@if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
<a class="box group"
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}">
<div class="flex flex-col mx-6">
<div class="box-title">{{ $destination->name }}</div>
<div class="box-description">server: {{ $destination->server->name }}</div>
</div>
</a>
@endif
@if ($destination->getMorphClass() === 'App\Models\SwarmDocker')
<a class="box group"
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}">
<div class="flex flex-col mx-6">
<div class="box-title">{{ $destination->name }}</div>
<div class="box-description">server: {{ $destination->server->name }}</div>
</div>
</a>
@endif
@empty
<div>
@if ($servers->count() === 0)
<div>No servers found. Please add one first.</div>
@else
<div>No destinations found.</div>
@endif
</div>
@endforelse
</div>
</x-layout>

View File

@@ -1,3 +0,0 @@
<x-layout>
<livewire:destination.form :destination="$destination" />
</x-layout>

View File

@@ -2,4 +2,4 @@
{{ Illuminate\Mail\Markdown::parse('---') }} {{ Illuminate\Mail\Markdown::parse('---') }}
{{ Illuminate\Mail\Markdown::parse($debug) }} {{-- {{ Illuminate\Mail\Markdown::parse($debug) }} --}}

View File

@@ -6,26 +6,25 @@
<x-forms.input wire:model="search" placeholder="Search for a user" /> <x-forms.input wire:model="search" placeholder="Search for a user" />
<x-forms.button type="submit">Search</x-forms.button> <x-forms.button type="submit">Search</x-forms.button>
</form> </form>
<h3 class="pt-4">Active Subscribers</h3> <div class="pt-4">Active Subscribers : {{ $activeSubscribers }}</div>
<div class="flex flex-wrap gap-2"> <div>Inactive Subscribers : {{ $inactiveSubscribers }}</div>
@forelse ($active_subscribers as $user) @if ($search)
<div class="flex gap-2 box" wire:click="switchUser('{{ $user->id }}')"> @if ($foundUsers->count() > 0)
<p>{{ $user->name }}</p> <div class="flex flex-wrap gap-2 pt-4">
<p>{{ $user->email }}</p> @foreach ($foundUsers as $user)
</div> <div class="box w-64 group">
@empty <div class="flex flex-col gap-2">
<p>No active subscribers</p> <div class="box-title">{{ $user->name }}</div>
@endforelse <div class="box-description">{{ $user->email }}</div>
</div> <div class="box-description">Active:
<h3 class="pt-4">Inactive Subscribers</h3> {{ $user->teams()->whereRelation('subscription', 'stripe_subscription_id', '!=', null)->exists() ? 'Yes' : 'No' }}
<div class="flex flex-col flex-wrap gap-2">
@forelse ($inactive_subscribers as $user)
<div class="flex gap-2 box" wire:click="switchUser('{{ $user->id }}')">
<p>{{ $user->name }}</p>
<p>{{ $user->email }}</p>
</div>
@empty
<p>No inactive subscribers</p>
@endforelse
</div> </div>
</div> </div>
</div>
@endforeach
</div>
@else
<div>No users found with {{ $search }}</div>
@endif
@endif
</div>

View File

@@ -94,7 +94,7 @@
@endforeach @endforeach
</div> </div>
@else @else
@if ($private_keys->count() === 0) @if ($privateKeys->count() === 0)
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div class='font-bold dark:text-warning'>No private keys found.</div> <div class='font-bold dark:text-warning'>No private keys found.</div>
<div class="flex items-center gap-1">Before you can add your server, first <x-modal-input <div class="flex items-center gap-1">Before you can add your server, first <x-modal-input
@@ -126,26 +126,17 @@
<section> <section>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h3 class="pb-2">Deployments</h3> <h3 class="pb-2">Deployments</h3>
@if (count($deployments_per_server) > 0) @if (count($deploymentsPerServer) > 0)
<x-loading /> <x-loading />
@endif @endif
<x-modal-confirmation <x-modal-confirmation title="Confirm Cleanup Queues?" buttonTitle="Cleanup Queues" isErrorButton
title="Confirm Cleanup Queues?" submitAction="cleanupQueue" :actions="['All running Deployment Queues will be cleaned up.']" :confirmWithText="false" :confirmWithPassword="false"
buttonTitle="Cleanup Queues" step2ButtonText="Permanently Cleanup Deployment Queues" :dispatchEvent="true"
isErrorButton dispatchEventType="success" dispatchEventMessage="Deployment Queues cleanup started." />
submitAction="cleanup_queue"
:actions="['All running Deployment Queues will be cleaned up.']"
:confirmWithText="false"
:confirmWithPassword="false"
step2ButtonText="Permanently Cleanup Deployment Queues"
:dispatchEvent="true"
dispatchEventType="success"
dispatchEventMessage="Deployment Queues cleanup started."
/>
</div> </div>
<div wire:poll.3000ms="get_deployments" class="grid grid-cols-1"> <div wire:poll.3000ms="loadDeployments" class="grid grid-cols-1">
@forelse ($deployments_per_server as $server_name => $deployments) @forelse ($deploymentsPerServer as $serverName => $deployments)
<h4 class="pb-2">{{ $server_name }}</h4> <h4 class="pb-2">{{ $serverName }}</h4>
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
@foreach ($deployments as $deployment) @foreach ($deployments as $deployment)
<a href="{{ data_get($deployment, 'deployment_url') }}" @class([ <a href="{{ data_get($deployment, 'deployment_url') }}" @class([
@@ -187,7 +178,4 @@
} }
} }
</script> </script>
{{-- <x-forms.button wire:click='getIptables'>Get IPTABLES</x-forms.button> --}}
</div> </div>

View File

@@ -1,29 +0,0 @@
<div>
<form class="flex flex-col">
<div class="flex items-center gap-2">
<h1>Destination</h1>
<x-forms.button wire:click.prevent='submit' type="submit">
Save
</x-forms.button>
@if ($destination->network !== 'coolify')
<x-modal-confirmation title="Confirm Destination Deletion?" buttonTitle="Delete Destination" isErrorButton
submitAction="delete" :actions="['This will delete the selected destination/network.']" confirmationText="{{ $destination->name }}"
confirmationLabel="Please confirm the execution of the actions by entering the Destination Name below"
shortConfirmationLabel="Destination Name" :confirmWithPassword="false" step2ButtonText="Permanently Delete" />
@endif
</div>
@if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
<div class="subtitle ">A Docker network in a non-swarm environment.</div>
@else
<div class="subtitle ">Your swarm docker network. WIP</div>
@endif
<div class="flex gap-2">
<x-forms.input id="destination.name" label="Name" />
<x-forms.input id="destination.server.ip" label="Server IP" readonly />
@if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
<x-forms.input id="destination.network" label="Docker Network" readonly />
@endif
</div>
</form>
</div>

View File

@@ -0,0 +1,44 @@
<div>
<x-slot:title>
Destinations | Coolify
</x-slot>
<div class="flex items-start gap-2">
<h1>Destinations</h1>
@if ($servers->count() > 0)
<x-modal-input buttonTitle="+ Add" title="New Destination">
<livewire:destination.new.docker />
</x-modal-input>
@endif
</div>
<div class="subtitle">Network endpoints to deploy your resources.</div>
<div class="grid gap-2 lg:grid-cols-1">
@forelse ($servers as $server)
@forelse ($server->destinations() as $destination)
@if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
<a class="box group"
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}"
wire:navigate>
<div class="flex flex-col mx-6">
<div class="box-title">{{ $destination->name }}</div>
<div class="box-description">Server: {{ $destination->server->name }}</div>
</div>
</a>
@endif
@if ($destination->getMorphClass() === 'App\Models\SwarmDocker')
<a class="box group"
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}"
wire:navigate>
<div class="flex flex-col mx-6">
<div class="box-title">{{ $destination->name }}</div>
<div class="box-description">server: {{ $destination->server->name }}</div>
</div>
</a>
@endif
@empty
<div>No destinations found.</div>
@endforelse
@empty
<div>No servers found.</div>
@endforelse
</div>
</div>

View File

@@ -5,7 +5,7 @@
<x-forms.input id="name" label="Name" required /> <x-forms.input id="name" label="Name" required />
<x-forms.input id="network" label="Network" required /> <x-forms.input id="network" label="Network" required />
</div> </div>
<x-forms.select id="server_id" label="Select a server" required wire:change="generate_name"> <x-forms.select id="serverId" label="Select a server" required wire:change="generateName">
<option disabled>Select a server</option> <option disabled>Select a server</option>
@foreach ($servers as $server) @foreach ($servers as $server)
<option value="{{ $server->id }}">{{ $server->name }}</option> <option value="{{ $server->id }}">{{ $server->name }}</option>

View File

@@ -1,42 +1,29 @@
<div> <div>
@if ($server->isFunctional()) <form class="flex flex-col">
<div class="flex items-end gap-2"> <div class="flex items-center gap-2">
<h2>Destinations</h2> <h1>Destination</h1>
<x-modal-input buttonTitle="+ Add" title="New Destination"> <x-forms.button wire:click.prevent='submit' type="submit">
<livewire:destination.new.docker :server_id="$server->id" /> Save
</x-modal-input> </x-forms.button>
<x-forms.button wire:click='scan'>Scan for Destinations</x-forms.button> @if ($network !== 'coolify')
</div> <x-modal-confirmation title="Confirm Destination Deletion?" buttonTitle="Delete Destination" isErrorButton
<div>Destinations are used to segregate resources by network.</div> submitAction="delete" :actions="['This will delete the selected destination/network.']" confirmationText="{{ $destination->name }}"
<div class="flex gap-2 pt-6"> confirmationLabel="Please confirm the execution of the actions by entering the Destination Name below"
Available for using: shortConfirmationLabel="Destination Name" :confirmWithPassword="false" step2ButtonText="Permanently Delete" />
@forelse ($server->standaloneDockers as $docker)
<a href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">
<button class="dark:text-white btn-link">{{ data_get($docker, 'network') }} </button>
</a>
@empty
@endforelse
@forelse ($server->swarmDockers as $docker)
<a href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">
<button class="dark:text-white btn-link">{{ data_get($docker, 'network') }} </button>
</a>
@empty
@endforelse
</div>
<div class="pt-2">
@if (count($networks) > 0)
<h3 class="pb-4">Found Destinations</h3>
@endif @endif
<div class="flex flex-wrap gap-2 ">
@foreach ($networks as $network)
<div class="min-w-fit">
<x-forms.button wire:click="add('{{ data_get($network, 'Name') }}')">Add
{{ data_get($network, 'Name') }}</x-forms.button>
</div>
@endforeach
</div>
</div> </div>
@if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
<div class="subtitle ">A simple Docker network.</div>
@else @else
<div>Server is not validated. Validate first.</div> <div class="subtitle ">A swarm Docker network. WIP</div>
@endif
<div class="flex gap-2">
<x-forms.input id="name" label="Name" />
<x-forms.input id="serverIp" label="Server IP" readonly />
@if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
<x-forms.input id="network" label="Docker Network" readonly />
@endif @endif
</div> </div>
</form>
</div>

View File

@@ -1,10 +1,11 @@
<div class="flex flex-col w-full gap-2"> <div class="flex flex-col w-full gap-2">
<div>Your feedback helps us to improve Coolify. Thank you! 💜</div> <div>Your feedback helps us to improve Coolify. Thank you! 💜</div>
<form wire:submit="submit" class="flex flex-col gap-4 pt-4"> <form wire:submit="submit" class="flex flex-col gap-4 pt-4">
<x-forms.input id="subject" label="Subject" placeholder="Summary of your problem."></x-forms.input> <x-forms.input minlength="3" required id="subject" label="Subject" placeholder="Help with..."></x-forms.input>
<x-forms.textarea rows="10" id="description" label="Description" class="font-sans" spellcheck <x-forms.textarea minlength="10" maxlength="1000" required rows="10" id="description" label="Description"
placeholder="Please provide as much information as possible."></x-forms.textarea> class="font-sans" spellcheck
placeholder="Having trouble with... Please provide as much information as possible."></x-forms.textarea>
<div></div> <div></div>
<x-forms.button class="w-full mt-4" type="submit" @click="modalOpen=false">Send</x-forms.button> <x-forms.button class="w-full mt-4" type="submit">Send</x-forms.button>
</form> </form>
</div> </div>

View File

@@ -9,7 +9,7 @@
<x-forms.button type="submit"> <x-forms.button type="submit">
Save Save
</x-forms.button> </x-forms.button>
@if ($team->discord_enabled) @if ($discordEnabled)
<x-forms.button class="normal-case dark:text-white btn btn-xs no-animation btn-primary" <x-forms.button class="normal-case dark:text-white btn btn-xs no-animation btn-primary"
wire:click="sendTestNotification"> wire:click="sendTestNotification">
Send Test Notifications Send Test Notifications
@@ -17,27 +17,26 @@
@endif @endif
</div> </div>
<div class="w-32"> <div class="w-32">
<x-forms.checkbox instantSave id="team.discord_enabled" label="Enabled" /> <x-forms.checkbox instantSave id="discordEnabled" label="Enabled" />
</div> </div>
<x-forms.input type="password" <x-forms.input type="password"
helper="Generate a webhook in Discord.<br>Example: https://discord.com/api/webhooks/...." required helper="Generate a webhook in Discord.<br>Example: https://discord.com/api/webhooks/...." required
id="team.discord_webhook_url" label="Webhook" /> id="discordWebhookUrl" label="Webhook" />
</form> </form>
@if (data_get($team, 'discord_enabled')) @if ($discordEnabled)
<h2 class="mt-4">Subscribe to events</h2> <h2 class="mt-4">Subscribe to events</h2>
<div class="w-64"> <div class="w-64">
@if (isDev()) @if (isDev())
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_test" label="Test" /> <x-forms.checkbox instantSave="saveModel" id="discordNotificationsTest" label="Test" />
@endif @endif
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_status_changes" <x-forms.checkbox instantSave="saveModel" id="discordNotificationsStatusChanges"
label="Container Status Changes" /> label="Container Status Changes" />
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_deployments" <x-forms.checkbox instantSave="saveModel" id="discordNotificationsDeployments"
label="Application Deployments" /> label="Application Deployments" />
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_database_backups" <x-forms.checkbox instantSave="saveModel" id="discordNotificationsDatabaseBackups" label="Backup Status" />
label="Backup Status" /> <x-forms.checkbox instantSave="saveModel" id="discordNotificationsScheduledTasks"
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_scheduled_tasks"
label="Scheduled Tasks Status" /> label="Scheduled Tasks Status" />
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_server_disk_usage" <x-forms.checkbox instantSave="saveModel" id="discordNotificationsServerDiskUsage"
label="Server Disk Usage" /> label="Server Disk Usage" />
</div> </div>
@endif @endif

View File

@@ -1,8 +1,9 @@
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'> <form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'>
<x-forms.input placeholder="Your Cool Project" id="name" label="Name" required /> <x-forms.input placeholder="Your Cool Project" id="name" label="Name" required />
<x-forms.input placeholder="This is my cool project everyone knows about" id="description" label="Description" /> <x-forms.input placeholder="This is my cool project everyone knows about" id="description" label="Description" />
<div class="subtitle">New project will have a default production environment.</div> <div class="subtitle">New project will have a default <span class="dark:text-warning font-bold">production</span>
<x-forms.button type="submit" @click="slideOverOpen=false"> environment.</div>
<x-forms.button type="submit">
Continue Continue
</x-forms.button> </x-forms.button>
</form> </form>

View File

@@ -1,6 +0,0 @@
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'>
<x-forms.input placeholder="production" id="name" label="Name" required />
<x-forms.button type="submit" @click="slideOverOpen=false">
Save
</x-forms.button>
</form>

View File

@@ -27,24 +27,22 @@
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input placeholder="coollabsio/coolify-example" id="application.git_repository" <x-forms.input placeholder="coollabsio/coolify-example" id="gitRepository" label="Repository" />
label="Repository" /> <x-forms.input placeholder="main" id="gitBranch" label="Branch" />
<x-forms.input placeholder="main" id="application.git_branch" label="Branch" />
</div> </div>
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<x-forms.input placeholder="HEAD" id="application.git_commit_sha" placeholder="HEAD" <x-forms.input placeholder="HEAD" id="gitCommitSha" placeholder="HEAD" label="Commit SHA" />
label="Commit SHA" />
</div> </div>
</div> </div>
@if (data_get($application, 'private_key_id')) @if ($privateKeyId)
<h3 class="pt-4">Deploy Key</h3> <h3 class="pt-4">Deploy Key</h3>
<div class="py-2 pt-4">Currently attached Private Key: <span <div class="py-2 pt-4">Currently attached Private Key: <span
class="dark:text-warning">{{ data_get($application, 'private_key.name') }}</span> class="dark:text-warning">{{ $privateKeyName }}</span>
</div> </div>
<h4 class="py-2 ">Select another Private Key</h4> <h4 class="py-2 ">Select another Private Key</h4>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
@foreach ($private_keys as $key) @foreach ($privateKeys as $key)
<x-forms.button wire:click.defer="setPrivateKey('{{ $key->id }}')">{{ $key->name }} <x-forms.button wire:click.defer="setPrivateKey('{{ $key->id }}')">{{ $key->name }}
</x-forms.button> </x-forms.button>
@endforeach @endforeach

View File

@@ -11,10 +11,9 @@
</div> </div>
</div> </div>
<div class="pt-2 pb-10">Edit project details here.</div> <div class="pt-2 pb-10">Edit project details here.</div>
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input label="Name" id="project.name" /> <x-forms.input label="Name" id="name" />
<x-forms.input label="Description" id="project.description" /> <x-forms.input label="Description" id="description" />
</div> </div>
</form> </form>
</div> </div>

View File

@@ -13,7 +13,7 @@
<li class="inline-flex items-center"> <li class="inline-flex items-center">
<div class="flex items-center"> <div class="flex items-center">
<a class="text-xs truncate lg:text-sm" <a class="text-xs truncate lg:text-sm"
href="{{ route('project.show', ['project_uuid' => data_get($parameters, 'project_uuid')]) }}"> href="{{ route('project.show', ['project_uuid' => $project->uuid]) }}">
{{ $project->name }}</a> {{ $project->name }}</a>
<svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold dark:text-warning" fill="currentColor" <svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold dark:text-warning" fill="currentColor"
viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
@@ -26,7 +26,9 @@
<li> <li>
<div class="flex items-center"> <div class="flex items-center">
<a class="text-xs truncate lg:text-sm" <a class="text-xs truncate lg:text-sm"
href="{{ route('project.resource.index', ['environment_name' => data_get($parameters, 'environment_name'), 'project_uuid' => data_get($parameters, 'project_uuid')]) }}">{{ data_get($parameters, 'environment_name') }}</a> href="{{ route('project.resource.index', ['environment_name' => $environment->name, 'project_uuid' => $project->uuid]) }}">
{{ $environment->name }}
</a>
</div> </div>
</li> </li>
<li> <li>
@@ -43,8 +45,8 @@
</ol> </ol>
</nav> </nav>
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input label="Name" id="environment.name" /> <x-forms.input label="Name" id="name" />
<x-forms.input label="Description" id="environment.description" /> <x-forms.input label="Description" id="description" />
</div> </div>
</form> </form>
</div> </div>

View File

@@ -5,13 +5,18 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h1>Environments</h1> <h1>Environments</h1>
<x-modal-input buttonTitle="+ Add" title="New Environment"> <x-modal-input buttonTitle="+ Add" title="New Environment">
<livewire:project.add-environment :project="$project" /> <form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'>
<x-forms.input placeholder="production" id="name" label="Name" required />
<x-forms.button type="submit">
Save
</x-forms.button>
</form>
</x-modal-input> </x-modal-input>
<livewire:project.delete-project :disabled="$project->resource_count() > 0" :project_id="$project->id" /> <livewire:project.delete-project :disabled="$project->resource_count() > 0" :project_id="$project->id" />
</div> </div>
<div class="text-xs truncate subtitle lg:text-sm">{{ $project->name }}.</div> <div class="text-xs truncate subtitle lg:text-sm">{{ $project->name }}.</div>
<div class="grid gap-2 lg:grid-cols-2"> <div class="grid gap-2 lg:grid-cols-2">
@forelse ($environments as $environment) @forelse ($project->environments->sortBy('created_at') as $environment)
<div class="gap-2 border border-transparent cursor-pointer box group" x-data <div class="gap-2 border border-transparent cursor-pointer box group" x-data
x-on:click="goto('{{ $project->uuid }}','{{ $environment->name }}')"> x-on:click="goto('{{ $project->uuid }}','{{ $environment->name }}')">
<div class="flex flex-1 mx-6"> <div class="flex flex-1 mx-6">
@@ -28,12 +33,6 @@
</a> </a>
</div> </div>
</div> </div>
{{-- <div class="flex items-center justify-center gap-2 pt-4 pb-2 mr-4 text-xs lg:py-0 lg:justify-normal">
<a class="mx-4 font-bold hover:underline"
href="{{ route('project.environment.edit', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => $environment->name]) }}">
Settings
</a>
</div> --}}
</div> </div>
@empty @empty
<p>No environments found.</p> <p>No environments found.</p>

View File

@@ -1,4 +1,7 @@
<div> <div>
<x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Advanced | Coolify
</x-slot>
<x-server.navbar :server="$server" /> <x-server.navbar :server="$server" />
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex flex-col h-full gap-8 sm:flex-row"> <div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex flex-col h-full gap-8 sm:flex-row">
<x-server.sidebar :server="$server" activeMenu="advanced" /> <x-server.sidebar :server="$server" activeMenu="advanced" />

View File

@@ -1,6 +1,6 @@
<div> <div>
<x-slot:title> <x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Server Destinations | Coolify {{ data_get_str($server, 'name')->limit(10) }} > Metrics | Coolify
</x-slot> </x-slot>
<x-server.navbar :server="$server" /> <x-server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row"> <div class="flex flex-col h-full gap-8 sm:flex-row">
@@ -8,6 +8,7 @@
<div class="w-full"> <div class="w-full">
<h2>Metrics</h2> <h2>Metrics</h2>
<div class="pb-4">Basic metrics for your container.</div> <div class="pb-4">Basic metrics for your container.</div>
@if ($server->isMetricsEnabled())
<div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()"> <div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()">
<x-forms.select label="Interval" wire:change="setInterval" id="interval"> <x-forms.select label="Interval" wire:change="setInterval" id="interval">
<option value="5">5 minutes (live)</option> <option value="5">5 minutes (live)</option>
@@ -248,6 +249,9 @@
</div> </div>
</div> </div>
@else
<div>Metrics are disabled for this server.</div>
@endif
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
<div> <div>
<x-slot:title> <x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Server Destinations | Coolify {{ data_get_str($server, 'name')->limit(10) }} > Destinations | Coolify
</x-slot> </x-slot>
<x-server.navbar :server="$server" /> <x-server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row"> <div class="flex flex-col h-full gap-8 sm:flex-row">

View File

@@ -1,6 +1,6 @@
<div> <div>
<x-slot:title> <x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Server Log Drains | Coolify {{ data_get_str($server, 'name')->limit(10) }} > Log Drains | Coolify
</x-slot> </x-slot>
<x-server.navbar :server="$server" /> <x-server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row"> <div class="flex flex-col h-full gap-8 sm:flex-row">

View File

@@ -1,6 +1,6 @@
<div> <div>
<x-slot:title> <x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Server Connection | Coolify {{ data_get_str($server, 'name')->limit(10) }} > Private Key | Coolify
</x-slot> </x-slot>
<x-server.navbar :server="$server" /> <x-server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row"> <div class="flex flex-col h-full gap-8 sm:flex-row">

View File

@@ -1,6 +1,6 @@
<div> <div>
<x-slot:title> <x-slot:title>
{{ data_get_str($server, 'name')->limit(10) }} > Server Configurations | Coolify {{ data_get_str($server, 'name')->limit(10) }} > General | Coolify
</x-slot> </x-slot>
<x-server.navbar :server="$server" /> <x-server.navbar :server="$server" />
<div class="flex flex-col h-full gap-8 sm:flex-row"> <div class="flex flex-col h-full gap-8 sm:flex-row">

View File

@@ -1,19 +1,21 @@
<div> <div>
<x-slot:title> <x-slot:title>
Settings | Coolify Transactional Email | Coolify
</x-slot> </x-slot>
<x-settings.navbar /> <x-settings.navbar />
<form wire:submit='submit' class="flex flex-col gap-2 pb-4">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h2>Transactional Email</h2> <h2>Transactional Email</h2>
</div>
<div class="pb-4 ">Email settings for password resets, invitations, etc.</div>
<form wire:submit='submitFromFields' class="flex flex-col gap-2 pb-4">
<x-forms.input required id="settings.smtp_from_name" helper="Name used in emails." label="From Name" />
<x-forms.input required id="settings.smtp_from_address" helper="Email address used in emails."
label="From Address" />
<x-forms.button type="submit"> <x-forms.button type="submit">
Save Save
</x-forms.button> </x-forms.button>
</div>
<div class="pb-4 ">Email settings for password resets, invitations, etc.</div>
<div class="flex gap-4">
<x-forms.input required id="smtpFromName" helper="Name used in emails." label="From Name" />
<x-forms.input required id="smtpFromAddress" helper="Email address used in emails." label="From Address" />
</div>
</form> </form>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="p-4 border dark:border-coolgray-300"> <div class="p-4 border dark:border-coolgray-300">
@@ -25,27 +27,26 @@
</x-forms.button> </x-forms.button>
</div> </div>
<div class="w-32"> <div class="w-32">
<x-forms.checkbox instantSave id="settings.smtp_enabled" label="Enabled" /> <x-forms.checkbox instantSave='instantSave("SMTP")' id="smtpEnabled" label="Enabled" />
</div> </div>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row"> <div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input required id="settings.smtp_host" placeholder="smtp.mailgun.org" label="Host" /> <x-forms.input required id="smtpHost" placeholder="smtp.mailgun.org" label="Host" />
<x-forms.input required id="settings.smtp_port" placeholder="587" label="Port" /> <x-forms.input required id="smtpPort" placeholder="587" label="Port" />
<x-forms.input id="settings.smtp_encryption" helper="If SMTP uses SSL, set it to 'tls'." <x-forms.input id="smtpEncryption" helper="If SMTP uses SSL, set it to 'tls'." placeholder="tls"
placeholder="tls" label="Encryption" /> label="Encryption" />
</div> </div>
<div class="flex flex-col w-full gap-2 xl:flex-row"> <div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input id="settings.smtp_username" label="SMTP Username" /> <x-forms.input id="smtpUsername" label="SMTP Username" />
<x-forms.input id="settings.smtp_password" type="password" label="SMTP Password" <x-forms.input id="smtpPassword" type="password" label="SMTP Password"
autocomplete="new-password" /> autocomplete="new-password" />
<x-forms.input id="settings.smtp_timeout" helper="Timeout value for sending emails." <x-forms.input id="smtpTimeout" helper="Timeout value for sending emails." label="Timeout" />
label="Timeout" />
</div> </div>
</div> </div>
</form> </form>
</div> </div>
<div class="p-4 border dark:border-coolgray-300"> <div class="p-4 border dark:border-coolgray-300">
<form wire:submit='submitResend' class="flex flex-col"> <form wire:submit='submit' class="flex flex-col">
<div class="flex gap-2"> <div class="flex gap-2">
<h3>Resend</h3> <h3>Resend</h3>
<x-forms.button type="submit"> <x-forms.button type="submit">
@@ -53,12 +54,12 @@
</x-forms.button> </x-forms.button>
</div> </div>
<div class="w-32"> <div class="w-32">
<x-forms.checkbox instantSave='instantSaveResend' id="settings.resend_enabled" label="Enabled" /> <x-forms.checkbox instantSave='instantSave("Resend")' id="resendEnabled" label="Enabled" />
</div> </div>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row"> <div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input type="password" id="settings.resend_api_key" placeholder="API key" required <x-forms.input type="password" id="resendApiKey" placeholder="API key" required label="API Key"
label="Host" autocomplete="new-password" /> autocomplete="new-password" />
</div> </div>
</div> </div>
</form> </form>

View File

@@ -149,6 +149,7 @@ Route::group([
} }
$data = request()->all(); $data = request()->all();
// \App\Jobs\ServerCheckNewJob::dispatch($server, $data);
PushServerUpdateJob::dispatch($server, $data); PushServerUpdateJob::dispatch($server, $data);
return response()->json(['message' => 'ok'], 200); return response()->json(['message' => 'ok'], 200);

View File

@@ -7,6 +7,8 @@ use App\Http\Controllers\UploadController;
use App\Livewire\Admin\Index as AdminIndex; use App\Livewire\Admin\Index as AdminIndex;
use App\Livewire\Boarding\Index as BoardingIndex; use App\Livewire\Boarding\Index as BoardingIndex;
use App\Livewire\Dashboard; use App\Livewire\Dashboard;
use App\Livewire\Destination\Index as DestinationIndex;
use App\Livewire\Destination\Show as DestinationShow;
use App\Livewire\Dev\Compose as Compose; use App\Livewire\Dev\Compose as Compose;
use App\Livewire\ForcePasswordReset; use App\Livewire\ForcePasswordReset;
use App\Livewire\Notifications\Discord as NotificationDiscord; use App\Livewire\Notifications\Discord as NotificationDiscord;
@@ -38,7 +40,7 @@ use App\Livewire\Server\Advanced as ServerAdvanced;
use App\Livewire\Server\Charts as ServerCharts; use App\Livewire\Server\Charts as ServerCharts;
use App\Livewire\Server\CloudflareTunnels; use App\Livewire\Server\CloudflareTunnels;
use App\Livewire\Server\Delete as DeleteServer; use App\Livewire\Server\Delete as DeleteServer;
use App\Livewire\Server\Destination\Show as DestinationShow; use App\Livewire\Server\Destinations as ServerDestinations;
use App\Livewire\Server\Index as ServerIndex; use App\Livewire\Server\Index as ServerIndex;
use App\Livewire\Server\LogDrains; use App\Livewire\Server\LogDrains;
use App\Livewire\Server\PrivateKey\Show as PrivateKeyShow; use App\Livewire\Server\PrivateKey\Show as PrivateKeyShow;
@@ -72,8 +74,6 @@ use App\Livewire\Terminal\Index as TerminalIndex;
use App\Models\GitlabApp; use App\Models\GitlabApp;
use App\Models\ScheduledDatabaseBackupExecution; use App\Models\ScheduledDatabaseBackupExecution;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use App\Providers\RouteServiceProvider; use App\Providers\RouteServiceProvider;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
@@ -98,14 +98,14 @@ Route::middleware(['throttle:login'])->group(function () {
Route::get('/auth/{provider}/redirect', [OauthController::class, 'redirect'])->name('auth.redirect'); Route::get('/auth/{provider}/redirect', [OauthController::class, 'redirect'])->name('auth.redirect');
Route::get('/auth/{provider}/callback', [OauthController::class, 'callback'])->name('auth.callback'); Route::get('/auth/{provider}/callback', [OauthController::class, 'callback'])->name('auth.callback');
Route::prefix('magic')->middleware(['auth'])->group(function () { // Route::prefix('magic')->middleware(['auth'])->group(function () {
Route::get('/servers', [MagicController::class, 'servers']); // Route::get('/servers', [MagicController::class, 'servers']);
Route::get('/destinations', [MagicController::class, 'destinations']); // Route::get('/destinations', [MagicController::class, 'destinations']);
Route::get('/projects', [MagicController::class, 'projects']); // Route::get('/projects', [MagicController::class, 'projects']);
Route::get('/environments', [MagicController::class, 'environments']); // Route::get('/environments', [MagicController::class, 'environments']);
Route::get('/project/new', [MagicController::class, 'newProject']); // Route::get('/project/new', [MagicController::class, 'newProject']);
Route::get('/environment/new', [MagicController::class, 'newEnvironment']); // Route::get('/environment/new', [MagicController::class, 'newEnvironment']);
}); // });
Route::middleware(['auth', 'verified'])->group(function () { Route::middleware(['auth', 'verified'])->group(function () {
Route::middleware(['throttle:force-password-reset'])->group(function () { Route::middleware(['throttle:force-password-reset'])->group(function () {
@@ -212,7 +212,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/private-key', PrivateKeyShow::class)->name('server.private-key'); Route::get('/private-key', PrivateKeyShow::class)->name('server.private-key');
Route::get('/resources', ResourcesShow::class)->name('server.resources'); Route::get('/resources', ResourcesShow::class)->name('server.resources');
Route::get('/cloudflare-tunnels', CloudflareTunnels::class)->name('server.cloudflare-tunnels'); Route::get('/cloudflare-tunnels', CloudflareTunnels::class)->name('server.cloudflare-tunnels');
Route::get('/destinations', DestinationShow::class)->name('server.destinations'); Route::get('/destinations', ServerDestinations::class)->name('server.destinations');
Route::get('/log-drains', LogDrains::class)->name('server.log-drains'); Route::get('/log-drains', LogDrains::class)->name('server.log-drains');
Route::get('/metrics', ServerCharts::class)->name('server.charts'); Route::get('/metrics', ServerCharts::class)->name('server.charts');
Route::get('/danger', DeleteServer::class)->name('server.delete'); Route::get('/danger', DeleteServer::class)->name('server.delete');
@@ -221,6 +221,8 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/proxy/logs', ProxyLogs::class)->name('server.proxy.logs'); Route::get('/proxy/logs', ProxyLogs::class)->name('server.proxy.logs');
Route::get('/terminal', ExecuteContainerCommand::class)->name('server.command'); Route::get('/terminal', ExecuteContainerCommand::class)->name('server.command');
}); });
Route::get('/destinations', DestinationIndex::class)->name('destination.index');
Route::get('/destination/{destination_uuid}', DestinationShow::class)->name('destination.show');
// Route::get('/security', fn () => view('security.index'))->name('security.index'); // Route::get('/security', fn () => view('security.index'))->name('security.index');
Route::get('/security/private-key', SecurityPrivateKeyIndex::class)->name('security.private-key.index'); Route::get('/security/private-key', SecurityPrivateKeyIndex::class)->name('security.private-key.index');
@@ -312,52 +314,7 @@ Route::middleware(['auth'])->group(function () {
return response()->json(['message' => $e->getMessage()], 500); return response()->json(['message' => $e->getMessage()], 500);
} }
})->name('download.backup'); })->name('download.backup');
Route::get('/destinations', function () {
$servers = Server::isUsable()->get();
$destinations = collect([]);
foreach ($servers as $server) {
$destinations = $destinations->merge($server->destinations());
}
$pre_selected_server_uuid = data_get(request()->query(), 'server');
if ($pre_selected_server_uuid) {
$server = $servers->firstWhere('uuid', $pre_selected_server_uuid);
if ($server) {
$server_id = $server->id;
}
}
return view('destination.all', [
'destinations' => $destinations,
'servers' => $servers,
'server_id' => $server_id ?? null,
]);
})->name('destination.all');
// Route::get('/destination/new', function () {
// $servers = Server::isUsable()->get();
// $pre_selected_server_uuid = data_get(request()->query(), 'server');
// if ($pre_selected_server_uuid) {
// $server = $servers->firstWhere('uuid', $pre_selected_server_uuid);
// if ($server) {
// $server_id = $server->id;
// }
// }
// return view('destination.new', [
// "servers" => $servers,
// "server_id" => $server_id ?? null,
// ]);
// })->name('destination.new');
Route::get('/destination/{destination_uuid}', function () {
$standalone_dockers = StandaloneDocker::where('uuid', request()->destination_uuid)->first();
$swarm_dockers = SwarmDocker::where('uuid', request()->destination_uuid)->first();
if (! $standalone_dockers && ! $swarm_dockers) {
abort(404);
}
$destination = $standalone_dockers ? $standalone_dockers : $swarm_dockers;
return view('destination.show', [
'destination' => $destination->load(['server']),
]);
})->name('destination.show');
}); });
Route::any('/{any}', function () { Route::any('/{any}', function () {