Compare commits

...

72 Commits

Author SHA1 Message Date
Andras Bacsai
272d5547c8 Merge pull request #1262 from coollabsio/next
v4.0.0-beta.49
2023-09-29 10:21:54 +02:00
Andras Bacsai
fbaec769f0 fix: reporting handler 2023-09-29 10:21:11 +02:00
Andras Bacsai
d93640a8d9 Merge pull request #1261 from coollabsio/next
v4.0.0-beta.48
2023-09-29 09:51:51 +02:00
Andras Bacsai
3749970831 fix: update process if server has been renamed 2023-09-29 09:48:20 +02:00
Andras Bacsai
7d45bb1335 version check 2023-09-28 22:55:03 +02:00
Andras Bacsai
da04e0c027 fix: sync:bunny 2023-09-28 22:42:10 +02:00
Andras Bacsai
c637d2d90d version++ 2023-09-28 22:31:53 +02:00
Andras Bacsai
1bb8860f37 Merge pull request #1260 from coollabsio/next
v4.0.0-beta.47
2023-09-28 22:25:12 +02:00
Andras Bacsai
38a22dcf4d fix: service templates 2023-09-28 22:20:49 +02:00
Andras Bacsai
91e1eb7664 fix 2023-09-28 21:56:34 +02:00
Andras Bacsai
c7946e7551 fix: next helper image 2023-09-28 21:52:50 +02:00
Andras Bacsai
958645e37f Merge branch 'main' into next 2023-09-28 21:48:03 +02:00
Andras Bacsai
acdfa89ec1 Update coolify-helper.yml 2023-09-28 21:32:53 +02:00
Andras Bacsai
62ec85ee2e Update coolify-helper.yml 2023-09-28 21:31:50 +02:00
Andras Bacsai
8f24a66456 Update coolify-helper.yml 2023-09-28 21:26:55 +02:00
Andras Bacsai
0dd0888775 Update coolify-helper.yml 2023-09-28 21:23:25 +02:00
Andras Bacsai
5040ddea28 fix: sentry 4502634789 2023-09-28 13:46:53 +02:00
Andras Bacsai
1a04a57c01 fix: sentry 4504136641 2023-09-28 13:40:58 +02:00
Andras Bacsai
9b6c162224 fix: sentry 4510197209 2023-09-28 13:15:14 +02:00
Andras Bacsai
e22c5d22f5 fix: instance fqdn setting 2023-09-28 13:13:21 +02:00
Andras Bacsai
84e5b39830 fix: prevent sync version (it needs an option) 2023-09-28 13:07:52 +02:00
Andras Bacsai
cdb6964b0b fix: install script drops an error 2023-09-28 13:05:17 +02:00
Andras Bacsai
df4ecd47a7 fix: sync command 2023-09-28 11:54:20 +02:00
Andras Bacsai
4a84c7238a fix: cannot delete env with available services 2023-09-28 11:51:01 +02:00
Andras Bacsai
2b3057e1b4 version++ 2023-09-28 11:50:50 +02:00
Andras Bacsai
ae65172946 Merge pull request #1256 from coollabsio/next
v4.0.0-beta.46
2023-09-28 11:38:16 +02:00
Andras Bacsai
3a2d17bc05 fix: containerStatusJob 2023-09-28 11:33:16 +02:00
Andras Bacsai
4c1067cf36 baseurl for docs 2023-09-28 11:01:00 +02:00
Andras Bacsai
b046a3e9f7 fix: sslip for localhost 2023-09-28 10:53:00 +02:00
Andras Bacsai
199881c596 fixes 2023-09-28 09:54:21 +02:00
Andras Bacsai
99c8607ff4 fix: disable early updates 2023-09-27 22:43:32 +02:00
Andras Bacsai
e61fcc77f9 ui 2023-09-27 22:39:10 +02:00
Andras Bacsai
20dca179fb add umami 2023-09-27 22:35:15 +02:00
Andras Bacsai
0f542c65ae fix: manually create network for services 2023-09-27 22:35:10 +02:00
Andras Bacsai
d609fcaee1 fix: services 2023-09-27 21:51:06 +02:00
Andras Bacsai
fe8a7fc54f fix: services view 2023-09-27 21:14:13 +02:00
Andras Bacsai
398f122593 fix: aaaaaaaaaaaaaaaaa 2023-09-27 15:48:19 +02:00
Andras Bacsai
f0abdcc2da okay, now it is way better 2023-09-27 12:45:53 +02:00
Andras Bacsai
c9a278b750 update service templates 2023-09-26 16:32:05 +02:00
Andras Bacsai
6990c593a4 update 2023-09-26 16:27:48 +02:00
Andras Bacsai
8f54b51ecd update templates 2023-09-26 16:21:55 +02:00
Andras Bacsai
3eb628b773 fix: containerstatusjob 2023-09-26 15:07:33 +02:00
Andras Bacsai
fabb97330a puh, fixes 2023-09-26 14:45:52 +02:00
Andras Bacsai
03c9793d11 Merge pull request #1252 from khrj/patch-1
Remove space before ? from onboarding flow
2023-09-25 21:02:53 +02:00
Andras Bacsai
a4320b7cee cloud: marketing emails 2023-09-25 20:57:52 +02:00
Andras Bacsai
cbf9bc99ea ui: help 2023-09-25 18:07:20 +02:00
Andras Bacsai
0fbc382467 feat: image tag for services 2023-09-25 17:51:04 +02:00
Andras Bacsai
0f8ccac775 fix: new service template layout 2023-09-25 17:41:35 +02:00
Andras Bacsai
ee20c3339e fix: show real storage name on services 2023-09-25 17:14:19 +02:00
Andras Bacsai
0b11093d18 feat: services 2023-09-25 15:48:43 +02:00
Khushraj Rathod
de8118b59d Remove space before ? from onboarding flow 2023-09-25 19:04:11 +05:30
Andras Bacsai
58522b59b7 - package updates
- feat: service fixes
- ui: help menu
2023-09-25 12:49:55 +02:00
Andras Bacsai
356394c03d ui: update help modal 2023-09-25 11:15:35 +02:00
Andras Bacsai
3e4db2f5b2 ui: more visible feedback button 2023-09-25 09:34:32 +02:00
Andras Bacsai
872981b8b4 ui: a bit better error 2023-09-25 09:20:29 +02:00
Andras Bacsai
51c468ae0b fix: proxy configuration + starter 2023-09-25 09:17:42 +02:00
Andras Bacsai
80a797aec8 version++ 2023-09-24 21:56:57 +02:00
Andras Bacsai
cfdab13d77 Merge pull request #1250 from coollabsio/next
v4.0.0-beta.45
2023-09-24 21:41:58 +02:00
Andras Bacsai
4fc8988ff4 fix: put back build pack chooser 2023-09-24 21:38:11 +02:00
Andras Bacsai
ab5619292e Merge pull request #1249 from chz/fix/1240
ui: Fixed z-index for magicbar
2023-09-24 21:18:48 +02:00
Andras Bacsai
b02e5d3f27 fix: applications with port mappins do a normal update (not rolling update) 2023-09-24 21:15:43 +02:00
Andras Bacsai
5f0c9c3a31 ui: Add source button 2023-09-24 21:15:21 +02:00
Chingiz Mammadov
ea6ec07a45 ui: Fixed z-index for magicbar 2023-09-24 23:03:11 +04:00
Andras Bacsai
36be325d0d Merge pull request #1248 from chz/ui/fix-version-zindex
ui: Fixed z-index for version link.
2023-09-24 20:42:35 +02:00
Andras Bacsai
6138ddcac6 revert sending notification 2023-09-24 17:51:01 +02:00
Andras Bacsai
0509da6730 fix: also check docker socks permission on validation 2023-09-24 17:48:40 +02:00
Andras Bacsai
5b877b84c2 fix: add expose port for containers 2023-09-24 17:48:25 +02:00
Andras Bacsai
0c35726a8d fix: add traefik labels no matter if traefik is selected or not 2023-09-24 17:47:43 +02:00
Andras Bacsai
e74899611b fix: links with path 2023-09-24 17:39:12 +02:00
Andras Bacsai
e9149e534d temporary disable containerstatusjob failed 2023-09-24 17:31:41 +02:00
Chingiz Mammadov
7cded7a36d ui: Fixed z-index for version link. 2023-09-24 14:12:07 +04:00
Andras Bacsai
ba74d55b4c fix: report livewire errors 2023-09-24 12:10:36 +02:00
114 changed files with 2422 additions and 1317 deletions

View File

@@ -4,7 +4,7 @@ on:
push: push:
branches: [ "next" ] branches: [ "next" ]
paths: paths:
- .github/workflows/coolify-helper.yml - .github/workflows/coolify-helper-next.yml
- docker/coolify-helper/Dockerfile - docker/coolify-helper/Dockerfile
env: env:

View File

@@ -77,7 +77,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Create & publish manifest - name: Create & publish manifest
run: | run: |
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- uses: sarisia/actions-status-discord@v1 - uses: sarisia/actions-status-discord@v1
if: always() if: always()
with: with:

View File

@@ -22,6 +22,7 @@ class StartProxy
if (!$configuration) { if (!$configuration) {
throw new \Exception("Configuration is not synced"); throw new \Exception("Configuration is not synced");
} }
SaveConfiguration::run($server, $configuration);
$docker_compose_yml_base64 = base64_encode($configuration); $docker_compose_yml_base64 = base64_encode($configuration);
$server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value; $server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save(); $server->save();
@@ -50,12 +51,15 @@ class StartProxy
"echo '####### Proxy installed successfully.'" "echo '####### Proxy installed successfully.'"
]); ]);
$commands = $commands->merge(connectProxyToNetworks($server)); $commands = $commands->merge(connectProxyToNetworks($server));
if (!$async) { if ($async) {
instant_remote_process($commands, $server);
return 'OK';
} else {
$activity = remote_process($commands, $server); $activity = remote_process($commands, $server);
return $activity; return $activity;
} else {
instant_remote_process($commands, $server);
$server->proxy->set('status', 'running');
$server->proxy->set('type', $proxyType);
$server->save();
return 'OK';
} }
} }
} }

View File

@@ -16,8 +16,7 @@ class UpdateCoolify
try { try {
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();
ray('Running InstanceAutoUpdateJob'); ray('Running InstanceAutoUpdateJob');
$localhost_name = 'localhost'; $this->server = Server::find(0)->first();
$this->server = Server::where('name', $localhost_name)->first();
if (!$this->server) { if (!$this->server) {
return; return;
} }

View File

@@ -10,22 +10,16 @@ class StartService
use AsAction; use AsAction;
public function handle(Service $service) public function handle(Service $service)
{ {
$workdir = service_configuration_dir() . "/{$service->uuid}"; $service->saveComposeConfigs();
$commands[] = "cd " . $service->workdir();
$commands[] = "echo '####### Saved configuration files to {$service->workdir()}.'";
$commands[] = "echo '####### Creating Docker network.'";
$commands[] = "docker network create --attachable {$service->uuid} >/dev/null 2>/dev/null || true";
$commands[] = "echo '####### Starting service {$service->name} on {$service->server->name}.'"; $commands[] = "echo '####### Starting service {$service->name} on {$service->server->name}.'";
$commands[] = "echo '####### Pulling images.'"; $commands[] = "echo '####### Pulling images.'";
$commands[] = "mkdir -p $workdir"; $commands[] = "docker compose pull";
$commands[] = "cd $workdir";
$docker_compose_base64 = base64_encode($service->docker_compose);
$commands[] = "echo $docker_compose_base64 | base64 -d > docker-compose.yml";
$envs = $service->environment_variables()->get();
$commands[] = "rm -f .env || true";
foreach ($envs as $env) {
$commands[] = "echo '{$env->key}={$env->value}' >> .env";
}
$commands[] = "docker compose pull --quiet";
$commands[] = "echo '####### Starting containers.'"; $commands[] = "echo '####### Starting containers.'";
$commands[] = "docker compose up -d >/dev/null 2>&1"; $commands[] = "docker compose up -d --remove-orphans --force-recreate";
$commands[] = "docker network connect $service->uuid coolify-proxy 2>/dev/null || true"; $commands[] = "docker network connect $service->uuid coolify-proxy 2>/dev/null || true";
$activity = remote_process($commands, $service->server); $activity = remote_process($commands, $service->server);
return $activity; return $activity;

View File

@@ -21,5 +21,6 @@ class StopService
$db->update(['status' => 'exited']); $db->update(['status' => 'exited']);
} }
instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy 2>/dev/null"], $service->server, false); instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy 2>/dev/null"], $service->server, false);
instant_remote_process(["docker network rm {$service->uuid} 2>/dev/null"], $service->server, false);
} }
} }

View File

@@ -56,6 +56,7 @@ class Emails extends Command
$type = select( $type = select(
'Which Email should be sent?', 'Which Email should be sent?',
options: [ options: [
'updates' => 'Send Update Email to all users',
'emails-test' => 'Test', 'emails-test' => 'Test',
'application-deployment-success' => 'Application - Deployment Success', 'application-deployment-success' => 'Application - Deployment Success',
'application-deployment-failed' => 'Application - Deployment Failed', 'application-deployment-failed' => 'Application - Deployment Failed',
@@ -69,7 +70,7 @@ class Emails extends Command
'realusers-server-lost-connection' => 'REAL - Server Lost Connection', 'realusers-server-lost-connection' => 'REAL - Server Lost Connection',
], ],
); );
$emailsGathered = ['realusers-before-trial','realusers-server-lost-connection']; $emailsGathered = ['realusers-before-trial', 'realusers-server-lost-connection'];
if (!in_array($type, $emailsGathered)) { if (!in_array($type, $emailsGathered)) {
$this->email = text('Email Address to send to'); $this->email = text('Email Address to send to');
} }
@@ -78,6 +79,38 @@ class Emails extends Command
$this->mail = new MailMessage(); $this->mail = new MailMessage();
$this->mail->subject("Test Email"); $this->mail->subject("Test Email");
switch ($type) { switch ($type) {
case 'updates':
$teams = Team::all();
if (!$teams || $teams->isEmpty()) {
echo 'No teams found.' . PHP_EOL;
return;
}
$emails = [];
foreach ($teams as $team) {
foreach ($team->members as $member) {
if ($member->email && $member->marketing_emails) {
$emails[] = $member->email;
}
}
}
$emails = array_unique($emails);
$this->info("Sending to " . count($emails) . " emails.");
foreach ($emails as $email) {
$this->info($email);
}
$confirmed = confirm('Are you sure?');
if ($confirmed) {
foreach ($emails as $email) {
$this->mail = new MailMessage();
$this->mail->subject('One-click Services, Docker Compose support');
$unsubscribeUrl = route('unsubscribe.marketing.emails', [
'token' => encrypt($email),
]);
$this->mail->view('emails.updates',["unsubscribeUrl" => $unsubscribeUrl]);
$this->sendEmail($email);
}
}
break;
case 'emails-test': case 'emails-test':
$this->mail = (new Test())->toMail(); $this->mail = (new Test())->toMail();
$this->sendEmail(); $this->sendEmail();
@@ -141,20 +174,20 @@ class Emails extends Command
$this->mail = (new BackupSuccess($backup, $db))->toMail(); $this->mail = (new BackupSuccess($backup, $db))->toMail();
$this->sendEmail(); $this->sendEmail();
break; break;
// case 'invitation-link': // case 'invitation-link':
// $user = User::all()->first(); // $user = User::all()->first();
// $invitation = TeamInvitation::whereEmail($user->email)->first(); // $invitation = TeamInvitation::whereEmail($user->email)->first();
// if (!$invitation) { // if (!$invitation) {
// $invitation = TeamInvitation::create([ // $invitation = TeamInvitation::create([
// 'uuid' => Str::uuid(), // 'uuid' => Str::uuid(),
// 'email' => $user->email, // 'email' => $user->email,
// 'team_id' => 1, // 'team_id' => 1,
// 'link' => 'http://example.com', // 'link' => 'http://example.com',
// ]); // ]);
// } // }
// $this->mail = (new InvitationLink($user))->toMail(); // $this->mail = (new InvitationLink($user))->toMail();
// $this->sendEmail(); // $this->sendEmail();
// break; // break;
case 'waitlist-invitation-link': case 'waitlist-invitation-link':
$this->mail = new MailMessage(); $this->mail = new MailMessage();
$this->mail->view('emails.waitlist-invitation', [ $this->mail->view('emails.waitlist-invitation', [

View File

@@ -7,6 +7,8 @@ use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Pool; use Illuminate\Http\Client\Pool;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use function Laravel\Prompts\confirm;
class SyncBunny extends Command class SyncBunny extends Command
{ {
/** /**
@@ -14,7 +16,7 @@ class SyncBunny extends Command
* *
* @var string * @var string
*/ */
protected $signature = 'sync:bunny'; protected $signature = 'sync:bunny {--only-template} {--only-version}';
/** /**
* The console command description. * The console command description.
@@ -28,6 +30,9 @@ class SyncBunny extends Command
*/ */
public function handle() public function handle()
{ {
$that = $this;
$only_template = $this->option('only-template');
$only_version = $this->option('only-version');
$bunny_cdn = "https://cdn.coollabs.io"; $bunny_cdn = "https://cdn.coollabs.io";
$bunny_cdn_path = "coolify"; $bunny_cdn_path = "coolify";
$bunny_cdn_storage_name = "coolcdn"; $bunny_cdn_storage_name = "coolcdn";
@@ -39,51 +44,71 @@ class SyncBunny extends Command
$install_script = "install.sh"; $install_script = "install.sh";
$upgrade_script = "upgrade.sh"; $upgrade_script = "upgrade.sh";
$production_env = ".env.production"; $production_env = ".env.production";
$service_template = "service-templates.json";
$versions = "versions.json"; $versions = "versions.json";
PendingRequest::macro('storage', function ($file) { PendingRequest::macro('storage', function ($fileName) use($that) {
$headers = [ $headers = [
'AccessKey' => env('BUNNY_STORAGE_API_KEY'), 'AccessKey' => env('BUNNY_STORAGE_API_KEY'),
'Accept' => 'application/json', 'Accept' => 'application/json',
'Content-Type' => 'application/octet-stream' 'Content-Type' => 'application/octet-stream'
]; ];
$fileStream = fopen($file, "r"); $fileStream = fopen($fileName, "r");
$file = fread($fileStream, filesize($file)); $file = fread($fileStream, filesize($fileName));
$that->info('Uploading: ' . $fileName);
return PendingRequest::baseUrl('https://storage.bunnycdn.com')->withHeaders($headers)->withBody($file)->throw(); return PendingRequest::baseUrl('https://storage.bunnycdn.com')->withHeaders($headers)->withBody($file)->throw();
}); });
PendingRequest::macro('purge', function ($url) { PendingRequest::macro('purge', function ($url) use ($that) {
$headers = [ $headers = [
'AccessKey' => env('BUNNY_API_KEY'), 'AccessKey' => env('BUNNY_API_KEY'),
'Accept' => 'application/json', 'Accept' => 'application/json',
]; ];
ray('Purging: ' . $url); $that->info('Purging: ' . $url);
return PendingRequest::withHeaders($headers)->get('https://api.bunny.net/purge', [ return PendingRequest::withHeaders($headers)->get('https://api.bunny.net/purge', [
"url" => $url, "url" => $url,
"async" => false "async" => false
]); ]);
}); });
try { try {
$confirmed = confirm('Are you sure you want to sync?');
if (!$confirmed) {
return;
}
if ($only_template) {
Http::pool(fn (Pool $pool) => [
$pool->storage(fileName: "$parent_dir/templates/$service_template")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$service_template"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$service_template"),
]);
$this->info('Service template uploaded & purged...');
return;
}
if ($only_version) {
Http::pool(fn (Pool $pool) => [
$pool->storage(fileName: "$parent_dir/$versions")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"),
]);
$this->info('versions.json uploaded & purged...');
return;
}
Http::pool(fn (Pool $pool) => [ Http::pool(fn (Pool $pool) => [
$pool->storage(file: "$parent_dir/$compose_file")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"), $pool->storage(fileName: "$parent_dir/$compose_file")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"),
$pool->storage(file: "$parent_dir/$compose_file_prod")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"), $pool->storage(fileName: "$parent_dir/$compose_file_prod")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"),
$pool->storage(file: "$parent_dir/$production_env")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$production_env"), $pool->storage(fileName: "$parent_dir/$production_env")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$production_env"),
$pool->storage(file: "$parent_dir/scripts/$upgrade_script")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$upgrade_script"), $pool->storage(fileName: "$parent_dir/scripts/$upgrade_script")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$upgrade_script"),
$pool->storage(file: "$parent_dir/scripts/$install_script")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$install_script"), $pool->storage(fileName: "$parent_dir/scripts/$install_script")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$install_script"),
$pool->storage(file: "$parent_dir/$versions")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"),
]); ]);
ray("{$bunny_cdn}/{$bunny_cdn_path}");
Http::pool(fn (Pool $pool) => [ Http::pool(fn (Pool $pool) => [
$pool->purge("$bunny_cdn/$bunny_cdn_path/$compose_file"), $pool->purge("$bunny_cdn/$bunny_cdn_path/$compose_file"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$compose_file_prod"), $pool->purge("$bunny_cdn/$bunny_cdn_path/$compose_file_prod"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$production_env"), $pool->purge("$bunny_cdn/$bunny_cdn_path/$production_env"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$upgrade_script"), $pool->purge("$bunny_cdn/$bunny_cdn_path/$upgrade_script"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$install_script"), $pool->purge("$bunny_cdn/$bunny_cdn_path/$install_script"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"),
]); ]);
echo "All files uploaded & purged...\n"; $this->info("All files uploaded & purged...");
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo $e->getMessage(); $this->error("Error: " . $e->getMessage());
} }
} }
} }

View File

@@ -40,8 +40,6 @@ class Kernel extends ConsoleKernel
private function check_resources($schedule) private function check_resources($schedule)
{ {
$servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true); $servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true);
ray($servers);
foreach ($servers as $server) { foreach ($servers as $server) {
$schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer(); $schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
} }

View File

@@ -55,10 +55,12 @@ class Handler extends ExceptionHandler
} }
app('sentry')->configureScope( app('sentry')->configureScope(
function (Scope $scope) { function (Scope $scope) {
$email = auth()?->user() ? auth()->user()->email : 'guest';
$instanceAdmin = User::find(0)->email ?? 'admin@localhost';
$scope->setUser( $scope->setUser(
[ [
'email' => auth()->user()->email, 'email' => $email,
'instanceAdmin' => User::find(0)->email 'instanceAdmin' => $instanceAdmin
] ]
); );
} }

View File

@@ -2,8 +2,12 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\EnvironmentVariable;
use App\Models\Project; use App\Models\Project;
use App\Models\Server; use App\Models\Server;
use App\Models\Service;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
class ProjectController extends Controller class ProjectController extends Controller
{ {
@@ -41,9 +45,10 @@ class ProjectController extends Controller
public function new() public function new()
{ {
$type = request()->query('type'); $services = getServiceTemplates();
$type = Str::of(request()->query('type'));
$destination_uuid = request()->query('destination'); $destination_uuid = request()->query('destination');
$server = requesT()->query('server'); $server_id = request()->query('server_id');
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) { if (!$project) {
@@ -61,8 +66,71 @@ class ProjectController extends Controller
'database_uuid' => $standalone_postgresql->uuid, 'database_uuid' => $standalone_postgresql->uuid,
]); ]);
} }
if ($type->startsWith('one-click-service-') && !is_null( (int)$server_id)) {
$oneClickServiceName = $type->after('one-click-service-')->value();
$oneClickService = data_get($services, "$oneClickServiceName.compose");
ray($oneClickServiceName);
$oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null);
if ($oneClickDotEnvs) {
$oneClickDotEnvs = Str::of(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/');
}
if ($oneClickService) {
$service = Service::create([
'name' => "$oneClickServiceName-" . Str::random(10),
'docker_compose_raw' => base64_decode($oneClickService),
'environment_id' => $environment->id,
'server_id' => (int) $server_id,
]);
$service->name = "$oneClickServiceName-" . $service->uuid;
$service->save();
if ($oneClickDotEnvs?->count() > 0) {
$oneClickDotEnvs->each(function ($value) use ($service) {
$key = Str::before($value, '=');
$value = Str::of(Str::after($value, '='));
$generatedValue = $value;
if ($value->contains('SERVICE_')) {
$command = $value->after('SERVICE_')->beforeLast('_');
switch ($command->value()) {
case 'PASSWORD':
$generatedValue = Str::password(symbols: false);
break;
case 'PASSWORD_64':
$generatedValue = Str::password(length: 64, symbols: false);
break;
case 'BASE64_64':
$generatedValue = Str::random(64);
break;
case 'BASE64_128':
$generatedValue = Str::random(128);
break;
case 'BASE64':
$generatedValue = Str::random(32);
break;
case 'USER':
$generatedValue = Str::random(16);
break;
}
}
EnvironmentVariable::create([
'key' => $key,
'value' => $generatedValue,
'service_id' => $service->id,
'is_build_time' => false,
'is_preview' => false,
]);
});
}
$service->parse(isNew: true);
return redirect()->route('project.service', [
'service_uuid' => $service->uuid,
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,
]);
}
}
return view('project.new', [ return view('project.new', [
'type' => $type 'type' => $type->value()
]); ]);
} }

View File

@@ -129,6 +129,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function selectExistingPrivateKey() public function selectExistingPrivateKey()
{ {
$this->createdPrivateKey = PrivateKey::find($this->selectedExistingPrivateKey); $this->createdPrivateKey = PrivateKey::find($this->selectedExistingPrivateKey);
$this->privateKey = $this->createdPrivateKey->private_key;
$this->currentState = 'create-server'; $this->currentState = 'create-server';
} }
public function createNewServer() public function createNewServer()

View File

@@ -4,8 +4,8 @@ namespace App\Http\Livewire;
use DanHarrin\LivewireRateLimiting\WithRateLimiting; use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Route;
use Livewire\Component; use Livewire\Component;
use Route;
class Help extends Component class Help extends Component
{ {
@@ -28,7 +28,7 @@ class Help extends Component
public function submit() public function submit()
{ {
try { try {
$this->rateLimit(1, 60); $this->rateLimit(3, 60);
$this->validate(); $this->validate();
$subscriptionType = auth()->user()?->subscription?->type() ?? 'Free'; $subscriptionType = auth()->user()?->subscription?->type() ?? 'Free';
$debug = "Route: {$this->path}"; $debug = "Route: {$this->path}";
@@ -42,7 +42,7 @@ class Help extends Component
); );
$mail->subject("[HELP - {$subscriptionType}]: {$this->subject}"); $mail->subject("[HELP - {$subscriptionType}]: {$this->subject}");
send_user_an_email($mail, auth()->user()?->email, 'hi@coollabs.io'); send_user_an_email($mail, auth()->user()?->email, 'hi@coollabs.io');
$this->emit('success', 'Your message has been sent successfully. We will get in touch with you as soon as possible.'); $this->emit('success', 'Your message has been sent successfully. <br>We will get in touch with you as soon as possible.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -2,73 +2,142 @@
namespace App\Http\Livewire\Project\New; namespace App\Http\Livewire\Project\New;
use App\Models\EnvironmentVariable;
use App\Models\Project; use App\Models\Project;
use App\Models\Service; use App\Models\Service;
use Livewire\Component; use Livewire\Component;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
class DockerCompose extends Component class DockerCompose extends Component
{ {
public string $dockercompose = ''; public string $dockerComposeRaw = '';
public string $envFile = '';
public array $parameters; public array $parameters;
public array $query; public array $query;
public function mount() public function mount()
{ {
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->query = request()->query(); $this->query = request()->query();
if (isDev()) { if (isDev()) {
$this->dockercompose = 'services: $this->dockerComposeRaw = 'services:
ghost: ghost:
documentation: https://ghost.org/docs/config image: ghost:5
image: ghost:5 volumes:
volumes: - ~/configs:/etc/configs/:ro
- ghost-content-data:/var/lib/ghost/content - ./var/lib/ghost/content:/tmp/ghost2/content:ro
environment: - /var/lib/ghost/content:/tmp/ghost/content:rw
- url=$SERVICE_FQDN_GHOST - ghost-content-data:/var/lib/ghost/content
- database__client=mysql - type: volume
- database__connection__host=mysql source: mydata
- database__connection__user=$SERVICE_USER_MYSQL target: /data
- database__connection__password=$SERVICE_PASSWORD_MYSQL volume:
- database__connection__database=${MYSQL_DATABASE-ghost} nocopy: true
depends_on: - type: bind
- mysql source: ./var/lib/ghost/data
mysql: target: /data
documentation: https://hub.docker.com/_/mysql - type: bind
image: mysql:8.0 source: /tmp
volumes: target: /tmp
- ghost-mysql-data:/var/lib/mysql labels:
environment: - "test.label=true"
- MYSQL_USER=${SERVICE_USER_MYSQL} ports:
- MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL} - "3000"
- MYSQL_DATABASE=${MYSQL_DATABASE} - "3000-3005"
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQL_ROOT} - "8000:8000"
'; - "9090-9091:8080-8081"
- "49100:22"
- "127.0.0.1:8001:8001"
- "127.0.0.1:5000-5010:5000-5010"
- "127.0.0.1::5000"
- "6060:6060/udp"
- "12400-12500:1240"
- target: 80
published: 8080
protocol: tcp
mode: host
networks:
- some-network
- other-network
environment:
- database__client=${DATABASE_CLIENT:-mysql}
- database__connection__database=${MYSQL_DATABASE:-ghost}
- database__connection__host=${DATABASE_CONNECTION_HOST:-mysql}
- test=${TEST:?true}
- url=$SERVICE_FQDN_GHOST
- database__connection__user=$SERVICE_USER_MYSQL
- database__connection__password=$SERVICE_PASSWORD_MYSQL
depends_on:
- mysql
mysql:
image: mysql:8.0
volumes:
- ghost-mysql-data:/var/lib/mysql
environment:
- MYSQL_USER=${SERVICE_USER_MYSQL}
- MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL}
- MYSQL_DATABASE=$MYSQL_DATABASE
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQLROOT}
- SESSION_SECRET
minio:
image: minio/minio
environment:
RACK_ENV: development
A: $A
SHOW: ${SHOW}
SHOW1: ${SHOW2-show1}
SHOW2: ${SHOW3:-show2}
SHOW3: ${SHOW4?show3}
SHOW4: ${SHOW5:?show4}
SHOW5: ${SERVICE_USER_MINIO}
SHOW6: ${SERVICE_PASSWORD_MINIO}
SHOW7: ${SERVICE_PASSWORD_64_MINIO}
SHOW8: ${SERVICE_BASE64_64_MINIO}
SHOW9: ${SERVICE_BASE64_128_MINIO}
SHOW10: ${SERVICE_BASE64_MINIO}
SHOW11:
';
} }
} }
public function submit() public function submit()
{ {
$this->validate([ try {
'dockercompose' => 'required' $this->validate([
]); 'dockerComposeRaw' => 'required'
$server_id = $this->query['server_id']; ]);
$this->dockerComposeRaw = Yaml::dump(Yaml::parse($this->dockerComposeRaw), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
$server_id = $this->query['server_id'];
$project = Project::where('uuid', $this->parameters['project_uuid'])->first(); $project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
$service = Service::create([
'name' => 'service' . Str::random(10),
'docker_compose_raw' => $this->dockerComposeRaw,
'environment_id' => $environment->id,
'server_id' => (int) $server_id,
]);
$variables = parseEnvFormatToArray($this->envFile);
foreach ($variables as $key => $variable) {
EnvironmentVariable::create([
'key' => $key,
'value' => $variable,
'is_build_time' => false,
'is_preview' => false,
'service_id' => $service->id,
]);
}
$service->name = "service-$service->uuid";
$service = Service::create([ $service->parse(isNew: true);
'name' => 'service' . Str::random(10),
'docker_compose_raw' => $this->dockercompose,
'environment_id' => $environment->id,
'server_id' => (int) $server_id,
]);
$service->name = "service-$service->uuid";
$service->parse(isNew: true); return redirect()->route('project.service', [
'service_uuid' => $service->uuid,
return redirect()->route('project.service', [ 'environment_name' => $environment->name,
'service_uuid' => $service->uuid, 'project_uuid' => $project->uuid,
'environment_name' => $environment->name, ]);
'project_uuid' => $project->uuid, } catch (\Throwable $e) {
]); return handleError($e, $this);
}
} }
} }

View File

@@ -144,10 +144,8 @@ class GithubPrivateRepository extends Component
$application->settings->is_static = $this->is_static; $application->settings->is_static = $this->is_static;
$application->settings->save(); $application->settings->save();
$application->fqdn = "http://{$application->uuid}.{$destination->server->ip}.sslip.io"; $sslip = sslip($destination->server);
if (isDev()) { $application->fqdn = "http://{$application->uuid}.$sslip";
$application->fqdn = "http://{$application->uuid}.127.0.0.1.sslip.io";
}
$application->name = generate_application_name($this->selected_repository_owner . '/' . $this->selected_repository_repo, $this->selected_branch_name, $application->uuid); $application->name = generate_application_name($this->selected_repository_owner . '/' . $this->selected_repository_repo, $this->selected_branch_name, $application->uuid);
$application->save(); $application->save();

View File

@@ -112,10 +112,8 @@ class GithubPrivateRepositoryDeployKey extends Component
$application->settings->is_static = $this->is_static; $application->settings->is_static = $this->is_static;
$application->settings->save(); $application->settings->save();
$application->fqdn = "http://{$application->uuid}.{$destination->server->ip}.sslip.io"; $sslip = sslip($destination->server);
if (isDev()) { $application->fqdn = "http://{$application->uuid}.$sslip";
$application->fqdn = "http://{$application->uuid}.127.0.0.1.sslip.io";
}
$application->name = generate_random_name($application->uuid); $application->name = generate_random_name($application->uuid);
$application->save(); $application->save();

View File

@@ -156,10 +156,8 @@ class PublicGitRepository extends Component
$application->settings->is_static = $this->is_static; $application->settings->is_static = $this->is_static;
$application->settings->save(); $application->settings->save();
$application->fqdn = "http://{$application->uuid}.{$destination->server->ip}.sslip.io"; $sslip = sslip($destination->server);
if (isDev()) { $application->fqdn = "http://{$application->uuid}.$sslip";
$application->fqdn = "http://{$application->uuid}.127.0.0.1.sslip.io";
}
$application->name = generate_application_name($this->git_repository, $this->git_branch, $application->uuid); $application->name = generate_application_name($this->git_repository, $this->git_branch, $application->uuid);
$application->save(); $application->save();

View File

@@ -3,12 +3,12 @@
namespace App\Http\Livewire\Project\New; namespace App\Http\Livewire\Project\New;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Countable; use Countable;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Livewire\Component; use Livewire\Component;
use Route;
class Select extends Component class Select extends Component
{ {
@@ -21,12 +21,16 @@ class Select extends Component
public Collection|array $standaloneDockers = []; public Collection|array $standaloneDockers = [];
public Collection|array $swarmDockers = []; public Collection|array $swarmDockers = [];
public array $parameters; public array $parameters;
public Collection|array $services = [];
public bool $loadingServices = true;
public bool $loading = false;
public ?string $existingPostgresqlUrl = null; public ?string $existingPostgresqlUrl = null;
protected $queryString = [ protected $queryString = [
'server', 'server',
]; ];
public function mount() public function mount()
{ {
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
@@ -44,9 +48,31 @@ class Select extends Component
// return handleError($e, $this); // return handleError($e, $this);
// } // }
// } // }
public function loadThings()
{
$this->loadServices();
$this->loadServers();
}
public function loadServices(bool $forceReload = false)
{
try {
if ($forceReload) {
Cache::forget('services');
}
$this->services = getServiceTemplates();
$this->emit('success', 'Successfully loaded services.');
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->loadingServices = false;
}
}
public function setType(string $type) public function setType(string $type)
{ {
$this->type = $type; $this->type = $type;
if ($this->loading) return;
$this->loading = true;
if ($type === "existing-postgresql") { if ($type === "existing-postgresql") {
$this->current_step = $type; $this->current_step = $type;
return; return;
@@ -87,7 +113,7 @@ class Select extends Component
]); ]);
} }
public function load_servers() public function loadServers()
{ {
$this->servers = Server::isUsable()->get(); $this->servers = Server::isUsable()->get();
} }

View File

@@ -60,13 +60,10 @@ CMD ["nginx", "-g", "daemon off;"]
'source_type' => GithubApp::class 'source_type' => GithubApp::class
]); ]);
$fqdn = "http://{$application->uuid}.{$destination->server->ip}.sslip.io"; $sslip = sslip($destination->server);
if (isDev()) {
$fqdn = "http://{$application->uuid}.127.0.0.1.sslip.io";
}
$application->update([ $application->update([
'name' => 'dockerfile-' . $application->uuid, 'name' => 'dockerfile-' . $application->uuid,
'fqdn' => $fqdn 'fqdn' => "http://{$application->uuid}.$sslip"
]); ]);
redirect()->route('project.application.configuration', [ redirect()->route('project.application.configuration', [

View File

@@ -8,23 +8,47 @@ use Livewire\Component;
class Application extends Component class Application extends Component
{ {
public ServiceApplication $application; public ServiceApplication $application;
public $parameters;
protected $rules = [ protected $rules = [
'application.human_name' => 'nullable', 'application.human_name' => 'nullable',
'application.description' => 'nullable', 'application.description' => 'nullable',
'application.fqdn' => 'nullable', 'application.fqdn' => 'nullable',
'application.image' => 'required',
'application.exclude_from_status' => 'required|boolean',
'application.required_fqdn' => 'required|boolean',
]; ];
public function render() public function render()
{ {
ray($this->application->fileStorages()->get());
return view('livewire.project.service.application'); return view('livewire.project.service.application');
} }
public function instantSave()
{
$this->submit();
}
public function delete()
{
try {
$this->application->delete();
$this->emit('success', 'Application deleted successfully.');
return redirect()->route('project.service', $this->parameters);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function mount()
{
$this->parameters = get_route_parameters();
}
public function submit() public function submit()
{ {
try { try {
$this->validate(); $this->validate();
$this->application->save(); $this->application->save();
updateCompose($this->application);
$this->emit('success', 'Application saved successfully.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
ray($e); return handleError($e, $this);
} finally { } finally {
$this->emit('generateDockerCompose'); $this->emit('generateDockerCompose');
} }

View File

@@ -2,26 +2,41 @@
namespace App\Http\Livewire\Project\Service; namespace App\Http\Livewire\Project\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
use Livewire\Component; use Livewire\Component;
class Database extends Component class Database extends Component
{ {
public ServiceDatabase $database; public ServiceDatabase $database;
public $fileStorages;
protected $listeners = ["refreshFileStorages"];
protected $rules = [ protected $rules = [
'database.human_name' => 'nullable', 'database.human_name' => 'nullable',
'database.description' => 'nullable', 'database.description' => 'nullable',
'database.image' => 'required',
'database.exclude_from_status' => 'required|boolean',
]; ];
public function render() public function render()
{ {
return view('livewire.project.service.database'); return view('livewire.project.service.database');
} }
public function mount() {
$this->refreshFileStorages();
}
public function instantSave() {
$this->submit();
}
public function refreshFileStorages()
{
$this->fileStorages = $this->database->fileStorages()->get();
}
public function submit() public function submit()
{ {
try { try {
$this->validate(); $this->validate();
$this->database->save(); $this->database->save();
updateCompose($this->database);
$this->emit('success', 'Database saved successfully.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
ray($e); ray($e);
} finally { } finally {

View File

@@ -3,17 +3,49 @@
namespace App\Http\Livewire\Project\Service; namespace App\Http\Livewire\Project\Service;
use App\Models\LocalFileVolume; use App\Models\LocalFileVolume;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Livewire\Component; use Livewire\Component;
use Illuminate\Support\Str;
class FileStorage extends Component class FileStorage extends Component
{ {
public LocalFileVolume $fileStorage; public LocalFileVolume $fileStorage;
public ServiceApplication|ServiceDatabase $service;
public string $fs_path;
protected $rules = [ protected $rules = [
'fileStorage.is_directory' => 'required',
'fileStorage.fs_path' => 'required', 'fileStorage.fs_path' => 'required',
'fileStorage.mount_path' => 'required', 'fileStorage.mount_path' => 'required',
'fileStorage.content' => 'nullable', 'fileStorage.content' => 'nullable',
]; ];
public function mount()
{
$this->service = $this->fileStorage->service;
$this->fs_path = Str::of($this->fileStorage->fs_path)->beforeLast('/');
$file = Str::of($this->fileStorage->fs_path)->afterLast('/');
if (Str::of($this->fs_path)->startsWith('.')) {
$this->fs_path = Str::of($this->fs_path)->after('.');
$this->fs_path = $this->service->service->workdir() . $this->fs_path . "/" . $file;
}
}
public function submit()
{
try {
$this->validate();
$this->fileStorage->save();
$this->service->saveFileVolumes();
$this->emit('success', 'File updated successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
$this->submit();
}
public function render() public function render()
{ {
return view('livewire.project.service.file-storage'); return view('livewire.project.service.file-storage');

View File

@@ -2,45 +2,63 @@
namespace App\Http\Livewire\Project\Service; namespace App\Http\Livewire\Project\Service;
use App\Jobs\ContainerStatusJob;
use App\Models\Service; use App\Models\Service;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Livewire\Component; use Livewire\Component;
class Index extends Component class Index extends Component
{ {
public Service $service; public Service $service;
public $applications;
public $databases;
public array $parameters; public array $parameters;
public array $query; public array $query;
protected $rules = [ protected $rules = [
'service.docker_compose_raw' => 'required', 'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required', 'service.docker_compose' => 'required',
'service.name' => 'required', 'service.name' => 'required',
'service.description' => 'required', 'service.description' => 'nullable',
]; ];
public function checkStatus() {
dispatch_sync(new ContainerStatusJob($this->service->server));
$this->refreshStack();
}
public function refreshStack()
{
$this->applications = $this->service->applications->sort();
$this->applications->each(function ($application) {
$application->refresh();
});
$this->databases = $this->service->databases->sort();
$this->databases->each(function ($database) {
$database->refresh();
});
}
public function mount() public function mount()
{ {
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->query = request()->query(); $this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail(); $this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
$this->refreshStack();
} }
public function render() public function render()
{ {
return view('livewire.project.service.index'); return view('livewire.project.service.index');
} }
public function save() { public function submit()
$this->service->save(); {
$this->service->parse();
$this->service->refresh();
$this->emit('refreshEnvs');
}
public function submit() {
try { try {
$this->validate(); $this->validate();
$this->service->save(); $this->service->save();
$this->service->parse();
$this->service->refresh();
$this->service->saveComposeConfigs();
$this->refreshStack();
$this->emit('refreshEnvs');
$this->emit('success', 'Service saved successfully.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
} }
} }

View File

@@ -39,5 +39,6 @@ class Navbar extends Component
{ {
StopService::run($this->service); StopService::run($this->service);
$this->service->refresh(); $this->service->refresh();
$this->emit('success', 'Service stopped successfully.');
} }
} }

View File

@@ -11,8 +11,8 @@ use Livewire\Component;
class Show extends Component class Show extends Component
{ {
public Service $service; public Service $service;
public ServiceApplication $serviceApplication; public ?ServiceApplication $serviceApplication = null;
public ServiceDatabase $serviceDatabase; public ?ServiceDatabase $serviceDatabase = null;
public array $parameters; public array $parameters;
public array $query; public array $query;
public Collection $services; public Collection $services;

View File

@@ -20,6 +20,7 @@ class Danger extends Component
public function delete() public function delete()
{ {
// Should be queued
try { try {
if ($this->resource->type() === 'service') { if ($this->resource->type() === 'service') {
$server = $this->resource->server; $server = $this->resource->server;

View File

@@ -9,13 +9,13 @@ class Add extends Component
public $parameters; public $parameters;
public bool $is_preview = false; public bool $is_preview = false;
public string $key; public string $key;
public string $value; public ?string $value = null;
public bool $is_build_time = false; public bool $is_build_time = false;
protected $listeners = ['clearAddEnv' => 'clear']; protected $listeners = ['clearAddEnv' => 'clear'];
protected $rules = [ protected $rules = [
'key' => 'required|string', 'key' => 'required|string',
'value' => 'required|string', 'value' => 'nullable',
'is_build_time' => 'required|boolean', 'is_build_time' => 'required|boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
@@ -32,6 +32,7 @@ class Add extends Component
public function submit() public function submit()
{ {
$this->validate(); $this->validate();
ray($this->key, $this->value, $this->is_build_time);
$this->emitUp('submit', [ $this->emitUp('submit', [
'key' => $this->key, 'key' => $this->key,
'value' => $this->value, 'value' => $this->value,

View File

@@ -53,7 +53,6 @@ class All extends Component
$this->resource->environment_variables_preview()->delete(); $this->resource->environment_variables_preview()->delete();
} else { } else {
$variables = parseEnvFormatToArray($this->variables); $variables = parseEnvFormatToArray($this->variables);
ray($variables);
$existingVariables = $this->resource->environment_variables(); $existingVariables = $this->resource->environment_variables();
$this->resource->environment_variables()->delete(); $this->resource->environment_variables()->delete();
} }
@@ -110,11 +109,16 @@ class All extends Component
$environment->is_build_time = $data['is_build_time']; $environment->is_build_time = $data['is_build_time'];
$environment->is_preview = $data['is_preview']; $environment->is_preview = $data['is_preview'];
if ($this->resource->type() === 'application') { switch ($this->resource->type()) {
$environment->application_id = $this->resource->id; case 'application':
} $environment->application_id = $this->resource->id;
if ($this->resource->type() === 'standalone-postgresql') { break;
$environment->standalone_postgresql_id = $this->resource->id; case 'standalone-postgresql':
$environment->standalone_postgresql_id = $this->resource->id;
break;
case 'service':
$environment->service_id = $this->resource->id;
break;
} }
$environment->save(); $environment->save();
$this->refreshEnvs(); $this->refreshEnvs();

View File

@@ -5,17 +5,19 @@ namespace App\Http\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable; use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
use Illuminate\Support\Str;
class Show extends Component class Show extends Component
{ {
public $parameters; public $parameters;
public ModelsEnvironmentVariable $env; public ModelsEnvironmentVariable $env;
public ?string $modalId = null; public ?string $modalId = null;
public bool $isDisabled = false;
public string $type; public string $type;
protected $rules = [ protected $rules = [
'env.key' => 'required|string', 'env.key' => 'required|string',
'env.value' => 'required|string', 'env.value' => 'nullable',
'env.is_build_time' => 'required|boolean', 'env.is_build_time' => 'required|boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
@@ -26,6 +28,10 @@ class Show extends Component
public function mount() public function mount()
{ {
$this->isDisabled = false;
if (Str::of($this->env->key)->startsWith('SERVICE_FQDN') || Str::of($this->env->key)->startsWith('SERVICE_URL')) {
$this->isDisabled = true;
}
$this->modalId = new Cuid2(7); $this->modalId = new Cuid2(7);
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
} }

View File

@@ -11,6 +11,7 @@ class Show extends Component
public LocalPersistentVolume $storage; public LocalPersistentVolume $storage;
public bool $isReadOnly = false; public bool $isReadOnly = false;
public ?string $modalId = null; public ?string $modalId = null;
public ?string $realName = null;
protected $rules = [ protected $rules = [
'storage.name' => 'required|string', 'storage.name' => 'required|string',
@@ -25,6 +26,11 @@ class Show extends Component
public function mount() public function mount()
{ {
if ($this->storage->resource_type === 'App\Models\ServiceApplication' || $this->storage->resource_type === 'App\Models\ServiceDatabase') {
$this->realName = "{$this->storage->service->service->uuid}_{$this->storage->name}";
} else {
$this->realName = $this->storage->name;
}
$this->modalId = new Cuid2(7); $this->modalId = new Cuid2(7);
} }

View File

@@ -38,8 +38,8 @@ class Proxy extends Component
public function select_proxy($proxy_type) public function select_proxy($proxy_type)
{ {
$this->server->proxy->type = $proxy_type; $this->server->proxy->set('status', 'exited');
$this->server->proxy->status = 'exited'; $this->server->proxy->set('type', $proxy_type);
$this->server->save(); $this->server->save();
$this->selectedProxy = $this->server->proxy->type; $this->selectedProxy = $this->server->proxy->type;
$this->emit('proxyStatusUpdated'); $this->emit('proxyStatusUpdated');

View File

@@ -11,6 +11,8 @@ class Modal extends Component
public function proxyStatusUpdated() public function proxyStatusUpdated()
{ {
$this->server->proxy->set('status', 'running');
$this->server->save();
$this->emit('proxyStatusUpdated'); $this->emit('proxyStatusUpdated');
} }
} }

View File

@@ -18,10 +18,8 @@ class Status extends Component
public function getProxyStatus() public function getProxyStatus()
{ {
try { try {
if ($this->server->isFunctional()) { dispatch_sync(new ContainerStatusJob($this->server));
dispatch_sync(new ContainerStatusJob($this->server)); $this->emit('proxyStatusUpdated');
$this->emit('proxyStatusUpdated');
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -14,8 +14,8 @@ class ShowPrivateKey extends Component
public function setPrivateKey($newPrivateKeyId) public function setPrivateKey($newPrivateKeyId)
{ {
try { try {
refresh_server_connection($this->server->privateKey);
$oldPrivateKeyId = $this->server->private_key_id; $oldPrivateKeyId = $this->server->private_key_id;
refresh_server_connection($this->server->privateKey);
$this->server->update([ $this->server->update([
'private_key_id' => $newPrivateKeyId 'private_key_id' => $newPrivateKeyId
]); ]);

View File

@@ -45,7 +45,12 @@ class Configuration extends Component
$this->settings->do_not_track = $this->do_not_track; $this->settings->do_not_track = $this->do_not_track;
$this->settings->is_auto_update_enabled = $this->is_auto_update_enabled; $this->settings->is_auto_update_enabled = $this->is_auto_update_enabled;
$this->settings->is_registration_enabled = $this->is_registration_enabled; $this->settings->is_registration_enabled = $this->is_registration_enabled;
$this->settings->next_channel = $this->next_channel; if ($this->next_channel) {
$this->settings->next_channel = false;
$this->next_channel = false;
} else {
$this->settings->next_channel = $this->next_channel;
}
$this->settings->save(); $this->settings->save();
$this->emit('success', 'Settings updated!'); $this->emit('success', 'Settings updated!');
} }
@@ -68,7 +73,7 @@ class Configuration extends Component
{ {
$file = "$this->dynamic_config_path/coolify.yaml"; $file = "$this->dynamic_config_path/coolify.yaml";
if (empty($this->settings->fqdn)) { if (empty($this->settings->fqdn)) {
remote_process([ instant_remote_process([
"rm -f $file", "rm -f $file",
], $this->server); ], $this->server);
} else { } else {
@@ -124,7 +129,6 @@ class Configuration extends Component
]; ];
} }
$this->save_configuration_to_disk($traefik_dynamic_conf, $file); $this->save_configuration_to_disk($traefik_dynamic_conf, $file);
dispatch(new ContainerStatusJob($this->server));
} }
} }
@@ -137,7 +141,7 @@ class Configuration extends Component
$yaml; $yaml;
$base64 = base64_encode($yaml); $base64 = base64_encode($yaml);
remote_process([ instant_remote_process([
"mkdir -p $this->dynamic_config_path", "mkdir -p $this->dynamic_config_path",
"echo '$base64' | base64 -d > $file", "echo '$base64' | base64 -d > $file",
], $this->server); ], $this->server);

View File

@@ -97,7 +97,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id); $this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id);
if ($this->application->fqdn) { if ($this->application->fqdn) {
$preview_fqdn = getOnlyFqdn(data_get($this->preview, 'fqdn')); $preview_fqdn = getFqdnWithoutPort(data_get($this->preview, 'fqdn'));
$template = $this->application->preview_url_template; $template = $this->application->preview_url_template;
$url = Url::fromString($this->application->fqdn); $url = Url::fromString($this->application->fqdn);
$host = $url->getHost(); $host = $url->getHost();
@@ -284,9 +284,20 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function rolling_update() private function rolling_update()
{ {
$this->start_by_compose_file(); if (count($this->application->ports_mappings_array) > 0){
$this->health_check(); $this->execute_remote_command(
$this->stop_running_container(); ["echo -n 'Application has ports mapped to the host system, rolling update is not supported. Stopping current container.'"],
);
$this->stop_running_container(force: true);
$this->start_by_compose_file();
} else {
$this->execute_remote_command(
["echo -n 'Rolling update started.'"],
);
$this->start_by_compose_file();
$this->health_check();
$this->stop_running_container();
}
} }
private function health_check() private function health_check()
{ {
@@ -529,7 +540,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
'restart' => RESTART_MODE, 'restart' => RESTART_MODE,
'environment' => $environment_variables, 'environment' => $environment_variables,
'labels' => generateLabelsApplication($this->application, $this->preview), 'labels' => generateLabelsApplication($this->application, $this->preview),
// 'expose' => $ports, 'expose' => $ports,
'networks' => [ 'networks' => [
$this->destination->network, $this->destination->network,
], ],
@@ -704,10 +715,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
} }
} }
private function stop_running_container() private function stop_running_container(bool $force = false)
{ {
if ($this->currently_running_container_name) { if ($this->currently_running_container_name) {
if ($this->newVersionIsHealthy) { if ($this->newVersionIsHealthy || $force) {
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'Removing old version of your application.'"], ["echo -n 'Removing old version of your application.'"],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true],
@@ -724,7 +735,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function start_by_compose_file() private function start_by_compose_file()
{ {
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'Rolling update started.'"], ["echo -n 'Starting application (could take a while).'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d >/dev/null"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d >/dev/null"), "hidden" => true],
); );
} }

View File

@@ -28,11 +28,12 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
public function __construct(public Server $server) public function __construct(public Server $server)
{ {
$this->handle();
} }
public function middleware(): array public function middleware(): array
{ {
return [new WithoutOverlapping($this->server->uuid)]; return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
} }
public function uniqueId(): string public function uniqueId(): string
@@ -74,14 +75,15 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$containers = format_docker_command_output_to_json($containers); $containers = format_docker_command_output_to_json($containers);
$applications = $this->server->applications(); $applications = $this->server->applications();
$databases = $this->server->databases(); $databases = $this->server->databases();
$services = $this->server->services(); $services = $this->server->services()->get();
$previews = $this->server->previews(); $previews = $this->server->previews();
$this->server->proxyType();
/// Check if proxy is running /// Check if proxy is running
$foundProxyContainer = $containers->filter(function ($value, $key) { $foundProxyContainer = $containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-proxy'; return data_get($value, 'Name') === '/coolify-proxy';
})->first(); })->first();
if (!$foundProxyContainer) { if (!$foundProxyContainer) {
ray('Proxy not found, starting it...');
if ($this->server->isProxyShouldRun()) { if ($this->server->isProxyShouldRun()) {
StartProxy::run($this->server, false); StartProxy::run($this->server, false);
$this->server->team->notify(new ContainerRestarted('coolify-proxy', $this->server)); $this->server->team->notify(new ContainerRestarted('coolify-proxy', $this->server));
@@ -99,7 +101,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
foreach ($containers as $container) { foreach ($containers as $container) {
$containerStatus = data_get($container, 'State.Status'); $containerStatus = data_get($container, 'State.Status');
$containerHealth = data_get($container, 'State.Health.Status','unhealthy'); $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
$containerStatus = "$containerStatus ($containerHealth)"; $containerStatus = "$containerStatus ($containerHealth)";
$labels = data_get($container, 'Config.Labels'); $labels = data_get($container, 'Config.Labels');
$labels = Arr::undot(format_docker_labels_to_json($labels)); $labels = Arr::undot(format_docker_labels_to_json($labels));
@@ -147,25 +149,29 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
} }
$serviceLabelId = data_get($labels, 'coolify.serviceId'); $serviceLabelId = data_get($labels, 'coolify.serviceId');
if ($serviceLabelId) { if ($serviceLabelId) {
$coolifyName = data_get($labels, 'coolify.name'); $subType = data_get($labels, 'coolify.service.subType');
$serviceName = Str::of($coolifyName)->before('-'); $subId = data_get($labels, 'coolify.service.subId');
$serviceUuid = Str::of($coolifyName)->after('-'); $service = $services->where('id', $serviceLabelId)->first();
$service = $services->where('uuid', $serviceUuid)->first(); if (!$service) {
continue;
}
if ($subType === 'application') {
$service = $service->applications()->where('id', $subId)->first();
} else {
$service = $service->databases()->where('id', $subId)->first();
}
if ($service) { if ($service) {
$foundService = $service->byName($serviceName); $foundServices[] = "$service->id-$service->name";
if ($foundService) { $statusFromDb = $service->status;
$foundServices[] = "$foundService->id-$serviceName"; if ($statusFromDb !== $containerStatus) {
$statusFromDb = $foundService->status; // ray('Updating status: ' . $containerStatus);
if ($statusFromDb !== $containerStatus) { $service->update(['status' => $containerStatus]);
// ray('Updating status: ' . $containerStatus);
$foundService->update(['status' => $containerStatus]);
}
} }
} }
} }
} }
$exitedServices = collect([]); $exitedServices = collect([]);
foreach ($services->get() as $service) { foreach ($services as $service) {
$apps = $service->applications()->get(); $apps = $service->applications()->get();
$dbs = $service->databases()->get(); $dbs = $service->databases()->get();
foreach ($apps as $app) { foreach ($apps as $app) {

View File

@@ -212,13 +212,9 @@ class Application extends BaseModel
{ {
if (data_get($this, 'private_key_id')) { if (data_get($this, 'private_key_id')) {
return 'deploy_key'; return 'deploy_key';
} } else if (data_get($this, 'source')) {
if (data_get($this, 'source')) {
return 'source'; return 'source';
} }
if (data_get($this, 'private_key_id')) {
return 'deploy_key';
}
throw new \Exception('No deployment type found'); throw new \Exception('No deployment type found');
} }
public function could_set_build_commands(): bool public function could_set_build_commands(): bool

View File

@@ -14,7 +14,7 @@ class Environment extends Model
public function can_delete_environment() public function can_delete_environment()
{ {
return $this->applications()->count() == 0 && $this->postgresqls()->count() == 0; return $this->applications()->count() == 0 && $this->postgresqls()->count() == 0 && $this->services()->count() == 0;
} }
public function applications() public function applications()

View File

@@ -11,6 +11,6 @@ class LocalFileVolume extends BaseModel
public function service() public function service()
{ {
return $this->morphTo(); return $this->morphTo('resource');
} }
} }

View File

@@ -12,16 +12,19 @@ class LocalPersistentVolume extends Model
public function application() public function application()
{ {
return $this->morphTo(); return $this->morphTo('resource');
} }
public function service() public function service()
{ {
return $this->morphTo(); return $this->morphTo('resource');
}
public function database()
{
return $this->morphTo('resource');
} }
public function standalone_postgresql() public function standalone_postgresql()
{ {
return $this->morphTo(); return $this->morphTo('resource');
} }
protected function name(): Attribute protected function name(): Attribute

View File

@@ -78,7 +78,8 @@ class Server extends BaseModel
return $this->hasOne(ServerSetting::class); return $this->hasOne(ServerSetting::class);
} }
public function proxyType() { public function proxyType()
{
$type = $this->proxy->get('type'); $type = $this->proxy->get('type');
if (is_null($type)) { if (is_null($type)) {
$this->proxy->type = ProxyTypes::TRAEFIK_V2->value; $this->proxy->type = ProxyTypes::TRAEFIK_V2->value;
@@ -115,11 +116,13 @@ class Server extends BaseModel
return $standaloneDocker->applications; return $standaloneDocker->applications;
})->flatten(); })->flatten();
} }
public function services() { public function services()
{
return $this->hasMany(Service::class); return $this->hasMany(Service::class);
} }
public function previews() { public function previews()
{
return $this->destinations()->map(function ($standaloneDocker) { return $this->destinations()->map(function ($standaloneDocker) {
return $standaloneDocker->applications->map(function ($application) { return $standaloneDocker->applications->map(function ($application) {
return $application->previews; return $application->previews;
@@ -161,6 +164,9 @@ class Server extends BaseModel
public function isProxyShouldRun() public function isProxyShouldRun()
{ {
$shouldRun = false; $shouldRun = false;
if ($this->proxyType() === ProxyTypes::NONE->value) {
return false;
}
foreach ($this->applications() as $application) { foreach ($this->applications() as $application) {
if (data_get($application, 'fqdn')) { if (data_get($application, 'fqdn')) {
$shouldRun = true; $shouldRun = true;
@@ -175,7 +181,8 @@ class Server extends BaseModel
} }
return $shouldRun; return $shouldRun;
} }
public function isFunctional() { public function isFunctional()
{
return $this->settings->is_reachable && $this->settings->is_usable; return $this->settings->is_reachable && $this->settings->is_usable;
} }
} }

View File

@@ -2,10 +2,10 @@
namespace App\Models; namespace App\Models;
use App\Enums\ProxyTypes;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Spatie\Url\Url; use Spatie\Url\Url;
@@ -41,6 +41,7 @@ class Service extends BaseModel
instant_remote_process(["docker volume rm -f $storage->name"], $service->server, false); instant_remote_process(["docker volume rm -f $storage->name"], $service->server, false);
}); });
} }
instant_remote_process(["docker network rm {$service->uuid}"], $service->server, false);
}); });
} }
public function type() public function type()
@@ -48,6 +49,12 @@ class Service extends BaseModel
return 'service'; return 'service';
} }
public function documentation()
{
$services = Cache::get('services', []);
$service = data_get($services, Str::of($this->name)->beforeLast('-')->value, []);
return data_get($service, 'documentation', config('constants.docs.base_url'));
}
public function applications() public function applications()
{ {
return $this->hasMany(ServiceApplication::class); return $this->hasMany(ServiceApplication::class);
@@ -80,9 +87,31 @@ class Service extends BaseModel
{ {
return $this->hasMany(EnvironmentVariable::class)->orderBy('key', 'asc'); return $this->hasMany(EnvironmentVariable::class)->orderBy('key', 'asc');
} }
public function workdir()
{
return service_configuration_dir() . "/{$this->uuid}";
}
public function saveComposeConfigs()
{
$workdir = $this->workdir();
$commands[] = "mkdir -p $workdir";
$commands[] = "cd $workdir";
$docker_compose_base64 = base64_encode($this->docker_compose);
$commands[] = "echo $docker_compose_base64 | base64 -d > docker-compose.yml";
$envs = $this->environment_variables()->get();
$commands[] = "rm -f .env || true";
foreach ($envs as $env) {
$commands[] = "echo '{$env->key}={$env->value}' >> .env";
}
if ($envs->count() === 0) {
$commands[] = "touch .env";
}
instant_remote_process($commands, $this->server);
}
public function parse(bool $isNew = false): Collection public function parse(bool $isNew = false): Collection
{ {
ray('parsing');
// ray()->clearAll(); // ray()->clearAll();
if ($this->docker_compose_raw) { if ($this->docker_compose_raw) {
try { try {
@@ -91,73 +120,95 @@ class Service extends BaseModel
throw new \Exception($e->getMessage()); throw new \Exception($e->getMessage());
} }
$composeVolumes = collect(data_get($yaml, 'volumes', [])); $topLevelVolumes = collect(data_get($yaml, 'volumes', []));
$composeNetworks = collect(data_get($yaml, 'networks', [])); $topLevelNetworks = collect(data_get($yaml, 'networks', []));
$dockerComposeVersion = data_get($yaml, 'version') ?? '3.8'; $dockerComposeVersion = data_get($yaml, 'version') ?? '3.8';
$services = data_get($yaml, 'services'); $services = data_get($yaml, 'services');
$definedNetwork = $this->uuid; $definedNetwork = $this->uuid;
$volumes = collect([]); $generatedServiceFQDNS = collect([]);
$envs = collect([]);
$ports = collect([]);
$services = collect($services)->map(function ($service, $serviceName) use ($composeVolumes, $composeNetworks, $definedNetwork, $envs, $volumes, $ports, $isNew) { $services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS) {
$container_name = "$serviceName-{$this->uuid}"; $serviceVolumes = collect(data_get($service, 'volumes', []));
$isDatabase = false; $servicePorts = collect(data_get($service, 'ports', []));
$serviceNetworks = collect(data_get($service, 'networks', []));
$serviceVariables = collect(data_get($service, 'environment', [])); $serviceVariables = collect(data_get($service, 'environment', []));
$serviceLabels = collect(data_get($service, 'labels', []));
$containerName = "$serviceName-{$this->uuid}";
// Decide if the service is a database // Decide if the service is a database
$image = data_get($service, 'image'); $isDatabase = false;
if ($image) { $image = data_get_str($service, 'image');
$imageName = Str::of($image)->before(':'); if ($image->contains(':')) {
if (collect(DATABASE_DOCKER_IMAGES)->contains($imageName)) { $image = Str::of($image);
$isDatabase = true; } else {
data_set($service, 'is_database', true); $image = Str::of($image)->append(':latest');
}
$imageName = $image->before(':');
if (collect(DATABASE_DOCKER_IMAGES)->contains($imageName)) {
$isDatabase = true;
}
data_set($service, 'is_database', $isDatabase);
// Create new serviceApplication or serviceDatabase
if ($isDatabase) {
if ($isNew) {
$savedService = ServiceDatabase::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $this->id
]);
} else {
$savedService = ServiceDatabase::where([
'name' => $serviceName,
'service_id' => $this->id
])->first();
}
} else {
if ($isNew) {
$savedService = ServiceApplication::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $this->id
]);
} else {
$savedService = ServiceApplication::where([
'name' => $serviceName,
'service_id' => $this->id
])->first();
} }
} }
if ($isNew) { if (is_null($savedService)) {
if ($isDatabase) { if ($isDatabase) {
$savedService = ServiceDatabase::create([ $savedService = ServiceDatabase::create([
'name' => $serviceName, 'name' => $serviceName,
'image' => $image,
'service_id' => $this->id 'service_id' => $this->id
]); ]);
} else { } else {
$defaultUsableFqdn = "http://$serviceName-{$this->uuid}.{$this->server->ip}.sslip.io";
if (isDev()) {
$defaultUsableFqdn = "http://$serviceName-{$this->uuid}.127.0.0.1.sslip.io";
}
$savedService = ServiceApplication::create([ $savedService = ServiceApplication::create([
'name' => $serviceName, 'name' => $serviceName,
'fqdn' => $defaultUsableFqdn, 'image' => $image,
'service_id' => $this->id 'service_id' => $this->id
]); ]);
} }
} else { }
if ($isDatabase) {
$savedService = $this->databases()->whereName($serviceName)->first(); // Collect/create/update networks
} else { if ($serviceNetworks->count() > 0) {
$savedService = $this->applications()->whereName($serviceName)->first(); foreach ($serviceNetworks as $networkName => $networkDetails) {
if (data_get($savedService, 'fqdn')) { $networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) {
$defaultUsableFqdn = data_get($savedService, 'fqdn', null); return $value == $networkName || $key == $networkName;
} else { });
if (Str::of($serviceVariables)->contains('SERVICE_FQDN') || Str::of($serviceVariables)->contains('SERVICE_URL')) { if (!$networkExists) {
$defaultUsableFqdn = "http://$serviceName-{$this->uuid}.{$this->server->ip}.sslip.io"; $topLevelNetworks->put($networkDetails, null);
if (isDev()) {
$defaultUsableFqdn = "http://$serviceName-{$this->uuid}.127.0.0.1.sslip.io";
}
}
} }
$savedService->fqdn = $defaultUsableFqdn;
$savedService->save();
} }
} }
$fqdns = data_get($savedService, 'fqdn');
if ($fqdns) { // Collect/create/update ports
$fqdns = collect(Str::of($fqdns)->explode(','));
}
// Collect ports
$servicePorts = collect(data_get($service, 'ports', []));
$ports->put($serviceName, $servicePorts);
$collectedPorts = collect([]); $collectedPorts = collect([]);
if ($servicePorts->count() > 0) { if ($servicePorts->count() > 0) {
foreach ($servicePorts as $sport) { foreach ($servicePorts as $sport) {
@@ -167,279 +218,288 @@ class Service extends BaseModel
if (is_array($sport)) { if (is_array($sport)) {
$target = data_get($sport, 'target'); $target = data_get($sport, 'target');
$published = data_get($sport, 'published'); $published = data_get($sport, 'published');
$collectedPorts->push("$target:$published"); $protocol = data_get($sport, 'protocol');
$collectedPorts->push("$target:$published/$protocol");
} }
} }
} }
$savedService->ports = $collectedPorts->implode(','); $savedService->ports = $collectedPorts->implode(',');
$savedService->save(); $savedService->save();
// Collect volumes
$serviceVolumes = collect(data_get($service, 'volumes', []));
if ($serviceVolumes->count() > 0) {
LocalPersistentVolume::whereResourceId($savedService->id)->whereResourceType(get_class($savedService))->delete();
foreach ($serviceVolumes as $volume) {
if (is_string($volume) && Str::startsWith($volume, './')) {
// Local file
$fsPath = Str::before($volume, ':');
$volumePath = Str::of($volume)->after(':')->beforeLast(':');
ray($fsPath, $volumePath);
LocalFileVolume::updateOrCreate(
[
'mount_path' => $volumePath,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
],
[
'fs_path' => $fsPath,
'mount_path' => $volumePath,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
]
);
continue;
}
if (is_string($volume)) {
$volumeName = Str::before($volume, ':');
$volumePath = Str::after($volume, ':');
}
if (is_array($volume)) {
$volumeName = data_get($volume, 'source');
$volumePath = data_get($volume, 'target');
}
$volumeExists = $serviceVolumes->contains(function ($_, $key) use ($volumeName) {
return $key == $volumeName;
});
if (!$volumeExists) {
if (Str::startsWith($volumeName, '/')) {
$volumes->put($volumeName, $volumePath);
LocalPersistentVolume::updateOrCreate(
[
'mount_path' => $volumePath,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
],
[
'name' => Str::slug($volumeName, '-'),
'mount_path' => $volumePath,
'host_path' => $volumeName,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
]
);
} else {
$composeVolumes->put($volumeName, null);
LocalPersistentVolume::updateOrCreate(
[
'name' => $volumeName,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
],
[
'name' => $volumeName,
'mount_path' => $volumePath,
'host_path' => null,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
]
);
}
}
}
}
// Collect and add networks
$serviceNetworks = collect(data_get($service, 'networks', []));
if ($serviceNetworks->count() > 0) {
foreach ($serviceNetworks as $networkName => $networkDetails) {
$networkExists = $composeNetworks->contains(function ($value, $key) use ($networkName) {
return $value == $networkName || $key == $networkName;
});
if (!$networkExists) {
$composeNetworks->put($networkDetails, null);
}
}
}
// Add Coolify specific networks // Add Coolify specific networks
$definedNetworkExists = $composeNetworks->contains(function ($value, $_) use ($definedNetwork) { $definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
return $value == $definedNetwork; return $value == $definedNetwork;
}); });
if (!$definedNetworkExists) { if (!$definedNetworkExists) {
$composeNetworks->put($definedNetwork, [ $topLevelNetworks->put($definedNetwork, [
'name' => $definedNetwork, 'name' => $definedNetwork,
'external' => false 'external' => true
]); ]);
} }
$networks = $serviceNetworks->toArray(); $networks = $serviceNetworks->toArray();
$networks = array_merge($networks, [$definedNetwork]); $networks = array_merge($networks, [$definedNetwork]);
data_set($service, 'networks', $networks); data_set($service, 'networks', $networks);
// Collect/create/update volumes
if ($serviceVolumes->count() > 0) {
foreach ($serviceVolumes as $volume) {
$type = null;
$source = null;
$target = null;
$content = null;
$isDirectory = false;
if (is_string($volume)) {
$source = Str::of($volume)->before(':');
$target = Str::of($volume)->after(':')->beforeLast(':');
if ($source->startsWith('./') || $source->startsWith('/') || $source->startsWith('~')) {
$type = Str::of('bind');
} else {
$type = Str::of('volume');
}
} else if (is_array($volume)) {
$type = data_get_str($volume, 'type');
$source = data_get_str($volume, 'source');
$target = data_get_str($volume, 'target');
$content = data_get($volume, 'content');
$isDirectory = (bool) data_get($volume, 'isDirectory', false);
$foundConfig = $savedService->fileStorages()->whereMountPath($target)->first();
if ($foundConfig) {
$content = data_get($foundConfig, 'content');
$isDirectory = (bool) data_get($foundConfig, 'is_directory');
}
}
if ($type->value() === 'bind') {
if ($source->value() === "/var/run/docker.sock") {
continue;
}
if ($source->value() === '/tmp' || $source->value() === '/tmp/') {
continue;
}
LocalFileVolume::updateOrCreate(
[
'mount_path' => $target,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
],
[
'fs_path' => $source,
'mount_path' => $target,
'content' => $content,
'is_directory' => $isDirectory,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
]
);
} else if ($type->value() === 'volume') {
$topLevelVolumes->put($source->value(), null);
LocalPersistentVolume::updateOrCreate(
[
'mount_path' => $target,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
],
[
'name' => Str::slug($source, '-'),
'mount_path' => $target,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
]
);
}
$savedService->saveFileVolumes();
}
}
// Add env_file with at least .env to the service
// $envFile = collect(data_get($service, 'env_file', []));
// if ($envFile->count() > 0) {
// if (!$envFile->contains('.env')) {
// $envFile->push('.env');
// }
// } else {
// $envFile = collect(['.env']);
// }
// data_set($service, 'env_file', $envFile->toArray());
// Get variables from the service // Get variables from the service
foreach ($serviceVariables as $variable) { foreach ($serviceVariables as $variableName => $variable) {
$value = Str::after($variable, '='); if (is_numeric($variableName)) {
if (!Str::startsWith($value, '$SERVICE_') && !Str::startsWith($value, '${SERVICE_') && Str::startsWith($value, '$')) { $variable = Str::of($variable);
$value = Str::of(replaceVariables(Str::of($value))); if ($variable->contains('=')) {
if ($value->contains(':')) { // - SESSION_SECRET=123
$nakedName = $value->before(':'); // - SESSION_SECRET=
$nakedValue = $value->after(':'); $key = $variable->before('=');
} else if ($value->contains('-')) { $value = $variable->after('=');
$nakedName = $value->before('-');
$nakedValue = $value->after('-');
} else if ($value->contains('+')) {
$nakedName = $value->before('+');
$nakedValue = $value->after('+');
} else { } else {
$nakedName = $value; // - SESSION_SECRET
$key = $variable;
$value = null;
} }
if (isset($nakedName)) { } else {
if (isset($nakedValue)) { // SESSION_SECRET: 123
if ($nakedValue->startsWith('-')) { // SESSION_SECRET:
$nakedValue = Str::of($nakedValue)->after('-'); $key = Str::of($variableName);
$value = Str::of($variable);
}
if ($key->startsWith('SERVICE_FQDN')) {
if (is_null(data_get($savedService, 'fqdn'))) {
$sslip = sslip($this->server);
$fqdn = "http://$containerName.$sslip";
if (substr_count($key->value(), '_') === 2 && $key->contains("=")) {
$path = $value->value();
if ($generatedServiceFQDNS->count() > 0) {
$alreadyGenerated = $generatedServiceFQDNS->has($key->value());
if ($alreadyGenerated) {
$fqdn = $generatedServiceFQDNS->get($key->value());
} else {
$generatedServiceFQDNS->put($key->value(), $fqdn);
}
} else {
$generatedServiceFQDNS->put($key->value(), $fqdn);
} }
if ($nakedValue->startsWith('+')) { $fqdn = "http://$containerName.$sslip$path";
$nakedValue = Str::of($nakedValue)->after('+'); }
} if (!$isDatabase) {
if (!$envs->has($nakedName->value())) { $savedService->fqdn = $fqdn;
$envs->put($nakedName->value(), $nakedValue->value()); $savedService->save();
EnvironmentVariable::updateOrCreate([ }
'key' => $nakedName->value(), }
'service_id' => $this->id, continue;
], [ }
'value' => $nakedValue->value(), if ($value?->startsWith('$')) {
$value = Str::of(replaceVariables($value));
$key = $value;
$foundEnv = EnvironmentVariable::where([
'key' => $key,
'service_id' => $this->id,
])->first();
if ($value->startsWith('SERVICE_')) {
$command = $value->after('SERVICE_')->beforeLast('_');
$forService = $value->afterLast('_');
$generatedValue = null;
if ($command->value() === 'FQDN' || $command->value() === 'URL') {
$sslip = sslip($this->server);
$fqdn = "http://$containerName.$sslip";
if ($foundEnv) {
$fqdn = data_get($foundEnv, 'value');
} else {
EnvironmentVariable::create([
'key' => $key,
'value' => $fqdn,
'is_build_time' => false, 'is_build_time' => false,
'service_id' => $this->id, 'service_id' => $this->id,
'is_preview' => false, 'is_preview' => false,
]); ]);
} }
if (!$isDatabase) {
$savedService->fqdn = $fqdn;
$savedService->save();
}
} else { } else {
if (!$envs->has($nakedName->value())) { switch ($command) {
$envs->put($nakedName->value(), null); case 'PASSWORD':
$envExists = EnvironmentVariable::where('service_id', $this->id)->where('key', $nakedName->value())->exists(); $generatedValue = Str::password(symbols: false);
if (!$envExists) { break;
EnvironmentVariable::create([ case 'PASSWORD_64':
'key' => $nakedName->value(), $generatedValue = Str::password(length: 64, symbols: false);
'value' => null, break;
'service_id' => $this->id, case 'BASE64_64':
'is_build_time' => false, $generatedValue = Str::random(64);
'is_preview' => false, break;
]); case 'BASE64_128':
} $generatedValue = Str::random(128);
break;
case 'BASE64':
$generatedValue = Str::random(32);
break;
case 'USER':
$generatedValue = Str::random(16);
break;
}
if (!$foundEnv) {
EnvironmentVariable::create([
'key' => $key,
'value' => $generatedValue,
'is_build_time' => false,
'service_id' => $this->id,
'is_preview' => false,
]);
} }
} }
} } else {
} else { if ($value->contains(':-')) {
$variableName = Str::of(replaceVariables(Str::of($value))); $key = $value->before(':');
$generatedValue = null; $defaultValue = $value->after(':-');
if ($variableName->startsWith('SERVICE_USER')) { } else if ($value->contains('-')) {
$variableDefined = EnvironmentVariable::whereServiceId($this->id)->where('key', $variableName->value())->first(); $key = $value->before('-');
if (!$variableDefined) { $defaultValue = $value->after('-');
$generatedValue = Str::random(10); } else if ($value->contains(':?')) {
$key = $value->before(':');
$defaultValue = $value->after(':?');
} else if ($value->contains('?')) {
$key = $value->before('?');
$defaultValue = $value->after('?');
} else { } else {
$generatedValue = $variableDefined->value; $key = $value;
$defaultValue = null;
} }
if (!$envs->has($variableName->value())) { if ($foundEnv) {
$envs->put($variableName->value(), $generatedValue); $defaultValue = data_get($foundEnv, 'value');
EnvironmentVariable::updateOrCreate([
'key' => $variableName->value(),
'service_id' => $this->id,
], [
'value' => $generatedValue,
'is_build_time' => false,
'service_id' => $this->id,
'is_preview' => false,
]);
}
} else if ($variableName->startsWith('SERVICE_PASSWORD')) {
$variableDefined = EnvironmentVariable::whereServiceId($this->id)->where('key', $variableName->value())->first();
if (!$variableDefined) {
$generatedValue = Str::password(symbols: false);
} else {
$generatedValue = $variableDefined->value;
}
if (!$envs->has($variableName->value())) {
$envs->put($variableName->value(), $generatedValue);
EnvironmentVariable::updateOrCreate([
'key' => $variableName->value(),
'service_id' => $this->id,
], [
'value' => $generatedValue,
'is_build_time' => false,
'service_id' => $this->id,
'is_preview' => false,
]);
}
} else if ($variableName->startsWith('SERVICE_FQDN')) {
if ($fqdns) {
$number = Str::of($variableName)->after('SERVICE_FQDN')->afterLast('_')->value();
if (is_numeric($number)) {
$number = (int) $number - 1;
} else {
$number = 0;
}
$fqdn = getOnlyFqdn(data_get($fqdns, $number, $fqdns->first()));
$environments = collect(data_get($service, 'environment'));
$environments = $environments->map(function ($envValue) use ($value, $fqdn) {
$envValue = Str::of($envValue)->replace($value, $fqdn);
return $envValue->value();
});
$service['environment'] = $environments->toArray();
}
} else if ($variableName->startsWith('SERVICE_URL')) {
if ($fqdns) {
$number = Str::of($variableName)->after('SERVICE_URL')->afterLast('_')->value();
if (is_numeric($number)) {
$number = (int) $number - 1;
} else {
$number = 0;
}
$fqdn = getOnlyFqdn(data_get($fqdns, $number, $fqdns->first()));
$url = Url::fromString($fqdn)->getHost();
$environments = collect(data_get($service, 'environment'));
$environments = $environments->map(function ($envValue) use ($value, $url) {
$envValue = Str::of($envValue)->replace($value, $url);
return $envValue->value();
});
$service['environment'] = $environments->toArray();
} }
EnvironmentVariable::updateOrCreate([
'key' => $key,
'service_id' => $this->id,
], [
'value' => $defaultValue,
'is_build_time' => false,
'service_id' => $this->id,
'is_preview' => false,
]);
} }
} }
} }
if ($this->server->proxyType() === ProxyTypes::TRAEFIK_V2->value) {
$labels = collect(data_get($service, 'labels', [])); // Add labels to the service
$labels = collect([]); $fqdns = collect(data_get($savedService, 'fqdns'));
$labels = $labels->merge(defaultLabels($this->id, $container_name, type: 'service')); $defaultLabels = defaultLabels($this->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id);
if (!$isDatabase) { $serviceLabels = $serviceLabels->merge($defaultLabels);
if ($fqdns) { if (!$isDatabase && $fqdns->count() > 0) {
$labels = $labels->merge(fqdnLabelsForTraefik($fqdns, $container_name, true)); if ($fqdns) {
} $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($fqdns, $containerName, true));
} }
data_set($service, 'labels', $labels->toArray());
} }
data_set($service, 'labels', $serviceLabels->toArray());
data_forget($service, 'is_database'); data_forget($service, 'is_database');
data_set($service, 'restart', RESTART_MODE); data_set($service, 'restart', RESTART_MODE);
data_set($service, 'container_name', $container_name); data_set($service, 'container_name', $containerName);
data_forget($service, 'documentation'); data_forget($service, 'volumes.*.content');
data_forget($service, 'volumes.*.isDirectory');
// Remove unnecessary variables from service.environment
$withoutServiceEnvs = collect([]);
collect(data_get($service, 'environment'))->each(function ($value, $key) use ($withoutServiceEnvs) {
if (!Str::of($key)->startsWith('$SERVICE_')) {
$withoutServiceEnvs->put($key, $value);
}
});
data_set($service, 'environment', $withoutServiceEnvs->toArray());
return $service; return $service;
}); });
$finalServices = [ $finalServices = [
'version' => $dockerComposeVersion, 'version' => $dockerComposeVersion,
'services' => $services->toArray(), 'services' => $services->toArray(),
'volumes' => $composeVolumes->toArray(), 'volumes' => $topLevelVolumes->toArray(),
'networks' => $composeNetworks->toArray(), 'networks' => $topLevelNetworks->toArray(),
]; ];
$this->docker_compose_raw = Yaml::dump($yaml, 10, 2);
$this->docker_compose = Yaml::dump($finalServices, 10, 2); $this->docker_compose = Yaml::dump($finalServices, 10, 2);
$this->save(); $this->save();
$shouldBeDefined = collect([ $this->saveComposeConfigs();
'envs' => $envs, return collect([]);
'volumes' => $volumes,
'ports' => $ports
]);
$parsedCompose = collect([
'dockerCompose' => $finalServices,
'shouldBeDefined' => $shouldBeDefined
]);
return $parsedCompose;
} else { } else {
return collect([]); return collect([]);
} }

View File

@@ -4,7 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Symfony\Component\Yaml\Yaml; use Illuminate\Support\Facades\Cache;
class ServiceApplication extends BaseModel class ServiceApplication extends BaseModel
{ {
@@ -15,10 +15,6 @@ class ServiceApplication extends BaseModel
{ {
return 'service'; return 'service';
} }
public function documentation()
{
return data_get(Yaml::parse($this->service->docker_compose_raw), "services.{$this->name}.documentation", 'https://coolify.io/docs');
}
public function service() public function service()
{ {
return $this->belongsTo(Service::class); return $this->belongsTo(Service::class);
@@ -31,4 +27,17 @@ class ServiceApplication extends BaseModel
{ {
return $this->morphMany(LocalFileVolume::class, 'resource'); return $this->morphMany(LocalFileVolume::class, 'resource');
} }
public function fqdns(): Attribute
{
return Attribute::make(
get: fn () => is_null($this->fqdn)
? []
: explode(',', $this->fqdn),
);
}
public function saveFileVolumes()
{
saveFileVolumesHelper($this);
}
} }

View File

@@ -14,10 +14,6 @@ class ServiceDatabase extends BaseModel
{ {
return 'service'; return 'service';
} }
public function documentation()
{
return data_get(Yaml::parse($this->service->docker_compose_raw), "services.{$this->name}.documentation", 'https://coolify.io/docs');
}
public function service() public function service()
{ {
return $this->belongsTo(Service::class); return $this->belongsTo(Service::class);
@@ -26,4 +22,12 @@ class ServiceDatabase extends BaseModel
{ {
return $this->morphMany(LocalPersistentVolume::class, 'resource'); return $this->morphMany(LocalPersistentVolume::class, 'resource');
} }
public function fileStorages()
{
return $this->morphMany(LocalFileVolume::class, 'resource');
}
public function saveFileVolumes()
{
saveFileVolumesHelper($this);
}
} }

View File

@@ -12,7 +12,7 @@ class GeneralNotification extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public $tries = 5; public $tries = 1;
public function __construct(public string $message) public function __construct(public string $message)
{ {
} }

View File

@@ -2,9 +2,6 @@
namespace App\Notifications; namespace App\Notifications;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;

View File

@@ -50,5 +50,8 @@ class RouteServiceProvider extends ServiceProvider
} }
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
}); });
RateLimiter::for('5', function (Request $request) {
return Limit::perMinute(5)->by($request->user()?->id ?: $request->ip());
});
} }
} }

View File

@@ -19,7 +19,7 @@ class Links extends Component
if ($application->fqdn) { if ($application->fqdn) {
$fqdns = collect(Str::of($application->fqdn)->explode(',')); $fqdns = collect(Str::of($application->fqdn)->explode(','));
$fqdns->map(function ($fqdn) { $fqdns->map(function ($fqdn) {
$this->links->push(getOnlyFqdn($fqdn)); $this->links->push(getFqdnWithoutPort($fqdn));
}); });
} }
if ($application->ports) { if ($application->ports) {

View File

@@ -21,5 +21,5 @@ const DATABASE_DOCKER_IMAGES = [
'couchdb', 'couchdb',
'neo4j', 'neo4j',
'influxdb', 'influxdb',
'clickhouse' 'clickhouse/clickhouse-server'
]; ];

View File

@@ -130,7 +130,7 @@ function get_port_from_dockerfile($dockerfile): int
return 80; return 80;
} }
function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'application') function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'application', $subType = null, $subId = null)
{ {
$labels = collect([]); $labels = collect([]);
$labels->push('coolify.managed=true'); $labels->push('coolify.managed=true');
@@ -141,13 +141,17 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$labels->push('coolify.pullRequestId=' . $pull_request_id); $labels->push('coolify.pullRequestId=' . $pull_request_id);
} }
if ($type === 'service') {
$labels->push('coolify.service.subId=' . $subId);
$labels->push('coolify.service.subType=' . $subType);
}
return $labels; return $labels;
} }
function fqdnLabelsForTraefik(Collection $domains, $container_name, $is_force_https_enabled) function fqdnLabelsForTraefik(Collection $domains, $container_name, $is_force_https_enabled)
{ {
$labels = collect([]); $labels = collect([]);
$labels->push('traefik.enable=true'); $labels->push('traefik.enable=true');
foreach($domains as $domain) { foreach ($domains as $domain) {
$url = Url::fromString($domain); $url = Url::fromString($domain);
$host = $url->getHost(); $host = $url->getHost();
$path = $url->getPath(); $path = $url->getPath();
@@ -216,9 +220,8 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
} else { } else {
$domains = Str::of(data_get($application, 'fqdn'))->explode(','); $domains = Str::of(data_get($application, 'fqdn'))->explode(',');
} }
if ($application->destination->server->proxy->type === ProxyTypes::TRAEFIK_V2->value) { // Add Traefik labels no matter which proxy is selected
$labels = $labels->merge(fqdnLabelsForTraefik($domains, $container_name, $application->settings->is_force_https_enabled)); $labels = $labels->merge(fqdnLabelsForTraefik($domains, $container_name, $application->settings->is_force_https_enabled));
}
} }
return $labels->all(); return $labels->all();
} }

View File

@@ -1,5 +1,6 @@
<?php <?php
use App\Actions\Proxy\SaveConfiguration;
use App\Models\Server; use App\Models\Server;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
@@ -10,7 +11,8 @@ function get_proxy_path()
$proxy_path = "$base_path/proxy"; $proxy_path = "$base_path/proxy";
return $proxy_path; return $proxy_path;
} }
function connectProxyToNetworks(Server $server) { function connectProxyToNetworks(Server $server)
{
$networks = collect($server->standaloneDockers)->map(function ($docker) { $networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network']; return $docker['network'];
})->unique(); })->unique();
@@ -20,7 +22,7 @@ function connectProxyToNetworks(Server $server) {
$commands = $networks->map(function ($network) { $commands = $networks->map(function ($network) {
return [ return [
"echo '####### Connecting coolify-proxy to $network network...'", "echo '####### Connecting coolify-proxy to $network network...'",
"docker network ls --format '{{.Name}}' | grep '^$network$' || docker network create --attachable $network >/dev/null", "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null",
"docker network connect $network coolify-proxy >/dev/null 2>&1 || true", "docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
]; ];
}); });
@@ -101,7 +103,9 @@ function generate_default_proxy_configuration(Server $server)
if (isDev()) { if (isDev()) {
$config['services']['traefik']['command'][] = "--log.level=debug"; $config['services']['traefik']['command'][] = "--log.level=debug";
} }
return Yaml::dump($config, 4, 2); $config = Yaml::dump($config, 4, 2);
SaveConfiguration::run($server, $config);
return $config;
} }
function setup_default_redirect_404(string|null $redirect_url, Server $server) function setup_default_redirect_404(string|null $redirect_url, Server $server)

View File

@@ -189,7 +189,7 @@ function validateServer(Server $server, bool $throwError = false)
]; ];
} }
$server->settings->is_reachable = true; $server->settings->is_reachable = true;
instant_remote_process(["docker ps"], $server, $throwError);
$dockerVersion = instant_remote_process(["docker version|head -2|grep -i version| awk '{print $2}'"], $server, $throwError); $dockerVersion = instant_remote_process(["docker version|head -2|grep -i version| awk '{print $2}'"], $server, $throwError);
if (!$dockerVersion) { if (!$dockerVersion) {
$dockerVersion = null; $dockerVersion = null;

View File

@@ -1,7 +1,12 @@
<?php <?php
use App\Models\EnvironmentVariable;
use App\Models\Service; use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
function replaceRegex(?string $name = null) function replaceRegex(?string $name = null)
{ {
@@ -20,27 +25,102 @@ function serviceStatus(Service $service)
{ {
$foundRunning = false; $foundRunning = false;
$isDegraded = false; $isDegraded = false;
$foundRestaring = false;
$applications = $service->applications; $applications = $service->applications;
$databases = $service->databases; $databases = $service->databases;
foreach ($applications as $application) { foreach ($applications as $application) {
if ($application->exclude_from_status) {
continue;
}
if (Str::of($application->status)->startsWith('running')) { if (Str::of($application->status)->startsWith('running')) {
$foundRunning = true; $foundRunning = true;
} else if (Str::of($application->status)->startsWith('restarting')) {
$foundRestaring = true;
} else { } else {
$isDegraded = true; $isDegraded = true;
} }
} }
foreach ($databases as $database) { foreach ($databases as $database) {
if ($database->exclude_from_status) {
continue;
}
if (Str::of($database->status)->startsWith('running')) { if (Str::of($database->status)->startsWith('running')) {
$foundRunning = true; $foundRunning = true;
} else if (Str::of($database->status)->startsWith('restarting')) {
$foundRestaring = true;
} else { } else {
$isDegraded = true; $isDegraded = true;
} }
} }
if ($foundRestaring) {
return 'degraded';
}
if ($foundRunning && !$isDegraded) { if ($foundRunning && !$isDegraded) {
return 'running'; return 'running';
} else if ($foundRunning && $isDegraded) { } else if ($foundRunning && $isDegraded) {
return 'degraded'; return 'degraded';
} else if (!$foundRunning && $isDegraded) { } else if (!$foundRunning && !$isDegraded) {
return 'exited'; return 'exited';
} }
return 'exited';
}
function saveFileVolumesHelper(ServiceApplication|ServiceDatabase $oneService)
{
try {
$workdir = $oneService->service->workdir();
$server = $oneService->service->server;
$applicationFileVolume = $oneService->fileStorages()->get();
$commands = collect([
"mkdir -p $workdir > /dev/null 2>&1 || true",
"cd $workdir"
]);
foreach ($applicationFileVolume as $fileVolume) {
$path = Str::of($fileVolume->fs_path);
if ($fileVolume->is_directory) {
$commands->push("test -f $path && rm -f $path > /dev/null 2>&1 || true");
$commands->push("mkdir -p $path > /dev/null 2>&1 || true");
continue;
}
$content = $fileVolume->content;
$dir = $path->beforeLast('/');
if ($dir->startsWith('.')) {
$dir = $dir->after('.');
$dir = $workdir . $dir;
}
$content = base64_encode($content);
$commands->push("test -d $path && rm -rf $path > /dev/null 2>&1 || true");
$commands->push("mkdir -p $dir > /dev/null 2>&1 || true");
$commands->push("echo '$content' | base64 -d > $path");
}
return instant_remote_process($commands, $server);
} catch (\Throwable $e) {
return handleError($e);
}
}
function updateCompose($resource) {
try {
$name = data_get($resource, 'name');
$dockerComposeRaw = data_get($resource, 'service.docker_compose_raw');
$dockerCompose = Yaml::parse($dockerComposeRaw);
// Switch Image
$image = data_get($resource, 'image');
data_set($dockerCompose, "services.{$name}.image", $image);
// Update FQDN
$variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper();
ray($variableName);
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
if ($generatedEnv){
$generatedEnv->value = $resource->fqdn;
$generatedEnv->save();
}
$dockerComposeRaw = Yaml::dump($dockerCompose, 10, 2);
$resource->service->docker_compose_raw = $dockerComposeRaw;
$resource->service->save();
} catch (\Throwable $e) {
return handleError($e);
}
} }

View File

@@ -1,6 +1,7 @@
<?php <?php
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\Server;
use App\Models\Team; use App\Models\Team;
use App\Models\User; use App\Models\User;
use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\DiscordChannel;
@@ -11,11 +12,13 @@ use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
use Illuminate\Database\QueryException; use Illuminate\Database\QueryException;
use Illuminate\Mail\Message; use Illuminate\Mail\Message;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Support\Stringable;
use Nubs\RandomNameGenerator\All; use Nubs\RandomNameGenerator\All;
use Poliander\Cron\CronExpression; use Poliander\Cron\CronExpression;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@@ -240,11 +243,13 @@ function base_ip(): string
} }
return "localhost"; return "localhost";
} }
function getOnlyFqdn(String $fqdn) { function getFqdnWithoutPort(String $fqdn)
{
$url = Url::fromString($fqdn); $url = Url::fromString($fqdn);
$host = $url->getHost(); $host = $url->getHost();
$scheme = $url->getScheme(); $scheme = $url->getScheme();
return "$scheme://$host"; $path = $url->getPath();
return "$scheme://$host$path";
} }
/** /**
* If fqdn is set, return it, otherwise return public ip. * If fqdn is set, return it, otherwise return public ip.
@@ -383,3 +388,36 @@ function parseEnvFormatToArray($env_file_contents)
} }
return $env_array; return $env_array;
} }
function data_get_str($data, $key, $default = null): Stringable
{
$str = data_get($data, $key, $default) ?? $default;
return Str::of($str);
}
function sslip(Server $server)
{
if (isDev()) {
return "127.0.0.1.sslip.io";
}
if ($server->ip === 'host.docker.internal') {
$baseIp = base_ip();
return "$baseIp.sslip.io";
}
return "{$server->ip}.sslip.io";
}
function getServiceTemplates()
{
if (isDev()) {
$services = File::get(base_path('templates/service-templates.json'));
$services = collect(json_decode($services))->sortKeys();
} else {
$services = Http::get(config('constants.services.official'));
if ($services->failed()) {
throw new \Exception($services->body());
}
$services = collect($services->json())->sortKeys();
}
return $services;
}

725
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,10 @@
<?php <?php
return [ return [
'ssh' =>[ 'docs' => [
'base_url' => 'https://coolify.io/docs',
'contact' => 'https://coolify.io/docs/contact',
],
'ssh' => [
'connection_timeout' => 10, 'connection_timeout' => 10,
'server_interval' => 20, 'server_interval' => 20,
'command_timeout' => 7200, 'command_timeout' => 7200,
@@ -14,8 +18,11 @@ return [
'expiration' => 10, 'expiration' => 10,
], ],
], ],
'services' => [
'official' => 'https://cdn.coollabs.io/coolify/service-templates.json',
],
'limits' => [ 'limits' => [
'trial_period'=> 7, 'trial_period' => 7,
'server' => [ 'server' => [
'zero' => 0, 'zero' => 0,
'self-hosted' => 999999999999, 'self-hosted' => 999999999999,

View File

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

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.44'; return '4.0.0-beta.49';

View File

@@ -0,0 +1,30 @@
<?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('services', function (Blueprint $table) {
$table->dropColumn('destination_type');
$table->dropColumn('destination_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('services', function (Blueprint $table) {
$table->morphs('destination');
});
}
};

View File

@@ -0,0 +1,32 @@
<?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('service_applications', function (Blueprint $table) {
$table->boolean('exclude_from_status')->default(false);
$table->boolean('required_fqdn')->default(false);
$table->string('image')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('service_applications', function (Blueprint $table) {
$table->dropColumn('exclude_from_status');
$table->dropColumn('required_fqdn');
$table->dropColumn('image');
});
}
};

View File

@@ -0,0 +1,30 @@
<?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('service_databases', function (Blueprint $table) {
$table->boolean('exclude_from_status')->default(false);
$table->string('image')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('service_databases', function (Blueprint $table) {
$table->dropColumn('exclude_from_status');
$table->dropColumn('image');
});
}
};

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
services:
fider:
image: getfider/fider:stable
environment:
BASE_URL: $SERVICE_FQDN_FIDER
DATABASE_URL: postgres://$SERVICE_USER_MYSQL:$SERVICE_PASSWORD_MYSQL@database:5432/fider?sslmode=disable
JWT_SECRET: $SERVICE_PASSWORD64_FIDER
EMAIL_NOREPLY: ${EMAIL_NOREPLY:-noreply@example.com}
EMAIL_MAILGUN_API: $EMAIL_MAILGUN_API
EMAIL_MAILGUN_DOMAIN: $EMAIL_MAILGUN_DOMAIN
EMAIL_MAILGUN_REGION: $EMAIL_MAILGUN_REGION
EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-smtp.mailgun.com}
EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-587}
EMAIL_SMTP_USERNAME: ${EMAIL_SMTP_USERNAME:-postmaster@mailgun.com}
EMAIL_SMTP_PASSWORD: $EMAIL_SMTP_PASSWORD
EMAIL_SMTP_ENABLE_STARTTLS: $EMAIL_SMTP_ENABLE_STARTTLS
EMAIL_AWSSES_REGION: $EMAIL_AWSSES_REGION
EMAIL_AWSSES_ACCESS_KEY_ID: $EMAIL_AWSSES_ACCESS_KEY_ID
EMAIL_AWSSES_SECRET_ACCESS_KEY: $EMAIL_AWSSES_SECRET_ACCESS_KEY
database:
image: postgres:12
volumes:
- pg_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: $SERVICE_USER_MYSQL
POSTGRES_PASSWORD: $SERVICE_PASSWORD_MYSQL
POSTGRES_DB: ${POSTGRES_DB:-fider}

View File

@@ -1,14 +1,8 @@
services: services:
ghost: ghost:
documentation: https://ghost.org/docs/config
image: ghost:5 image: ghost:5
volumes: volumes:
- ghost-content-data:/var/lib/ghost/content - ghost-content-data:/var/lib/ghost/content
- type: volume
source: /data/g
target: /data
volume:
nocopy: true
environment: environment:
- url=$SERVICE_FQDN_GHOST - url=$SERVICE_FQDN_GHOST
- database__client=mysql - database__client=mysql
@@ -16,24 +10,9 @@ services:
- database__connection__user=$SERVICE_USER_MYSQL - database__connection__user=$SERVICE_USER_MYSQL
- database__connection__password=$SERVICE_PASSWORD_MYSQL - database__connection__password=$SERVICE_PASSWORD_MYSQL
- database__connection__database=${MYSQL_DATABASE-ghost} - database__connection__database=${MYSQL_DATABASE-ghost}
networks:
default:
aliases:
- alias1
- alias3
ipv4_address: 172.16.238.10
ipv6_address: 2001:3984:3989::10
ports:
- "2368"
- 1234:2368
- target: 2368
published: 1234
protocol: tcp
mode: host
depends_on: depends_on:
- mysql - mysql
mysql: mysql:
documentation: https://hub.docker.com/_/mysql
image: mysql:8.0 image: mysql:8.0
volumes: volumes:
- ghost-mysql-data:/var/lib/mysql - ghost-mysql-data:/var/lib/mysql
@@ -42,10 +21,3 @@ services:
- MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL} - MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL}
- MYSQL_DATABASE=${MYSQL_DATABASE} - MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQL_ROOT} - MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQL_ROOT}
networks:
default:
ipam:
driver: default
config:
- subnet: "172.16.238.0/24"
- subnet: "2001:3984:3989::/64"

View File

@@ -0,0 +1,52 @@
version: "3.3"
services:
plausible:
image: plausible/analytics:v2.0
command: sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"
environment:
- DATABASE_URL=postgres://postgres:$SERVICE_PASSWORD_POSTGRES@plausible_db/plausible
- BASE_URL=$SERVICE_FQDN_PLAUSIBLE
- SECRET_KEY_BASE=$SERVICE_BASE64_64_PLAUSIBLE
depends_on:
- plausible_db
- plausible_events_db
- mail
mail:
image: bytemark/smtp
plausible_db:
image: postgres:14-alpine
volumes:
- db-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=plausible
- POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES
plausible_events_db:
image: clickhouse/clickhouse-server:23.3.7.5-alpine
volumes:
- type: volume
source: event-data
target: /var/lib/clickhouse
- type: bind
source: ./clickhouse/clickhouse-config.xml
target: /etc/clickhouse-server/config.d/logging.xml
read_only: true
content: >-
<clickhouse><profiles><default><log_queries>0</log_queries><log_query_threads>0</log_query_threads></default></profiles></clickhouse>
- type: bind
source: ./clickhouse/clickhouse-user-config.xml
target: /etc/clickhouse-server/users.d/logging.xml
read_only: true
content: >-
<clickhouse><logger><level>warning</level><console>true</console></logger><query_thread_log
remove="remove"/><query_log remove="remove"/><text_log
remove="remove"/><trace_log remove="remove"/><metric_log
remove="remove"/><asynchronous_metric_log
remove="remove"/><session_log remove="remove"/><part_log
remove="remove"/></clickhouse>
ulimits:
nofile:
soft: 262144
hard: 262144

View File

@@ -0,0 +1,15 @@
services:
postgres:
image: postgres
command: 'postgres -c config_file=/etc/postgresql/postgresql.conf'
volumes:
- type: bind
source: ./postgresql.conf
target: /etc/postgresql/postgresql.conf
- type: bind
source: ./docker-entrypoint-initdb.d
target: /docker-entrypoint-initdb.d/
isDirectory: true
environment:
POSTGRES_USER: $SERVICE_USER_POSTGRES
POSTGRES_PASSWORD: $SERVICE_PASSWORD_POSTGRES

View File

@@ -0,0 +1,5 @@
services:
uptime-kuma:
image: louislam/uptime-kuma:1
volumes:
- uptime-kuma:/app/data

View File

@@ -0,0 +1,76 @@
services:
ghost:
image: ghost:5
volumes:
- ~/configs:/etc/configs/:ro
- ./var/lib/ghost/content:/tmp/ghost2/content:ro
- /var/lib/ghost/content:/tmp/ghost/content:rw
- ghost-content-data:/var/lib/ghost/content
- type: volume
source: mydata
target: /data
volume:
nocopy: true
- type: bind
source: ./var/lib/ghost/data
target: /data
- type: bind
source: /tmp
target: /tmp
labels:
- "test.label=true"
ports:
- "3000"
- "3000-3005"
- "8000:8000"
- "9090-9091:8080-8081"
- "49100:22"
- "127.0.0.1:8001:8001"
- "127.0.0.1:5000-5010:5000-5010"
- "127.0.0.1::5000"
- "6060:6060/udp"
- "12400-12500:1240"
- target: 80
published: 8080
protocol: tcp
mode: host
networks:
- some-network
- other-network
environment:
- database__client=${DATABASE_CLIENT:-mysql}
- database__connection__database=${MYSQL_DATABASE:-ghost}
- database__connection__host=${DATABASE_CONNECTION_HOST:-mysql}
- test=${TEST:?true}
- url=$SERVICE_FQDN_GHOST
- database__connection__user=$SERVICE_USER_MYSQL
- database__connection__password=$SERVICE_PASSWORD_MYSQL
depends_on:
- mysql
mysql:
image: mysql:8.0
volumes:
- ghost-mysql-data:/var/lib/mysql
environment:
- MYSQL_USER=${SERVICE_USER_MYSQL}
- MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL}
- MYSQL_DATABASE=$MYSQL_DATABASE
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQLROOT}
- SESSION_SECRET
minio:
image: minio/minio
environment:
RACK_ENV: development
A: $A
SHOW: ${SHOW}
SHOW1: ${SHOW2-show1}
SHOW2: ${SHOW3:-show2}
SHOW3: ${SHOW4?show3}
SHOW4: ${SHOW5:?show4}
SHOW5: ${SERVICE_USER_MINIO}
SHOW6: ${SERVICE_PASSWORD_MINIO}
SHOW7: ${SERVICE_PASSWORD_64_MINIO}
SHOW8: ${SERVICE_BASE64_64_MINIO}
SHOW9: ${SERVICE_BASE64_128_MINIO}
SHOW10: ${SERVICE_BASE64_MINIO}
SHOW11:

352
package-lock.json generated
View File

@@ -5,19 +5,19 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"@tailwindcss/typography": "0.5.9", "@tailwindcss/typography": "0.5.10",
"alpinejs": "3.12.2", "alpinejs": "3.13.0",
"daisyui": "3.2.1", "daisyui": "3.7.7",
"tailwindcss-scrollbar": "0.1.0" "tailwindcss-scrollbar": "0.1.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "4.2.3", "@vitejs/plugin-vue": "4.3.4",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.16",
"axios": "1.4.0", "axios": "1.5.0",
"laravel-vite-plugin": "0.7.8", "laravel-vite-plugin": "0.8.0",
"postcss": "8.4.24", "postcss": "8.4.30",
"tailwindcss": "3.3.2", "tailwindcss": "3.3.3",
"vite": "4.3.9", "vite": "4.4.9",
"vue": "3.3.4" "vue": "3.3.4"
} }
}, },
@@ -45,9 +45,9 @@
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
"integrity": "sha512-E/sgkvwoIfj4aMAPL2e35VnUJspzVYl7+M1B2cqeubdBhADV4uPon0KCc8p2G+LqSJ6i8ocYPCqY3A4GGq0zkQ==", "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -61,9 +61,9 @@
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
"integrity": "sha512-WQ9p5oiXXYJ33F2EkE3r0FRDFVpEdcDiwNX3u7Xaibxfx6vQE0Sb8ytrfQsA5WO6kDn6mDfKLh6KrPBjvkk7xA==", "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -77,9 +77,9 @@
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
"integrity": "sha512-m4OsaCr5gT+se25rFPHKQXARMyAehHTQAz4XX1Vk3d27VtqiX0ALMBPoXZsGaB6JYryCLfgGwUslMqTfqeLU0w==", "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -93,9 +93,9 @@
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
"integrity": "sha512-O3GCZghRIx+RAN0NDPhyyhRgwa19MoKlzGonIb5hgTj78krqp9XZbYCvFr9N1eUxg0ZQEpiiZ4QvsOQwBpP+lg==", "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -109,9 +109,9 @@
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
"integrity": "sha512-5D48jM3tW27h1qjaD9UNRuN+4v0zvksqZSPZqeSWggfMlsVdAhH3pwSfQIFJwcs9QJ9BRibPS4ViZgs3d2wsCA==", "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -125,9 +125,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
"integrity": "sha512-OWvHzmLNTdF1erSvrfoEBGlN94IE6vCEaGEkEH29uo/VoONqPnoDFfShi41Ew+yKimx4vrmmAJEGNoyyP+OgOQ==", "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -141,9 +141,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
"integrity": "sha512-A0Xg5CZv8MU9xh4a+7NUpi5VHBKh1RaGJKqjxe4KG87X+mTjDE6ZvlJqpWoeJxgfXHT7IMP9tDFu7IZ03OtJAw==", "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -157,9 +157,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
"integrity": "sha512-WsHyJ7b7vzHdJ1fv67Yf++2dz3D726oO3QCu8iNYik4fb5YuuReOI9OtA+n7Mk0xyQivNTPbl181s+5oZ38gyA==", "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -173,9 +173,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
"integrity": "sha512-cK3AjkEc+8v8YG02hYLQIQlOznW+v9N+OI9BAFuyqkfQFR+DnDLhEM5N8QRxAUz99cJTo1rLNXqRrvY15gbQUg==", "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -189,9 +189,9 @@
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
"integrity": "sha512-jdOBXJqcgHlah/nYHnj3Hrnl9l63RjtQ4vn9+bohjQPI2QafASB5MtHAoEv0JQHVb/xYQTFOeuHnNYE1zF7tYw==", "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -205,9 +205,9 @@
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
"integrity": "sha512-GTOEtj8h9qPKXCyiBBnHconSCV9LwFyx/gv3Phw0pa25qPYjVuuGZ4Dk14bGCfGX3qKF0+ceeQvwmtI+aYBbVA==", "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -221,9 +221,9 @@
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
"integrity": "sha512-o8CIhfBwKcxmEENOH9RwmUejs5jFiNoDw7YgS0EJTF6kgPgcqLFjgoc5kDey5cMHRVCIWc6kK2ShUePOcc7RbA==", "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@@ -237,9 +237,9 @@
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
"integrity": "sha512-biMLH6NR/GR4z+ap0oJYb877LdBpGac8KfZoEnDiBKd7MD/xt8eaw1SFfYRUeMVx519kVkAOL2GExdFmYnZx3A==", "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -253,9 +253,9 @@
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
"integrity": "sha512-jkphYUiO38wZGeWlfIBMB72auOllNA2sLfiZPGDtOBb1ELN8lmqBrlMiucgL8awBw1zBXN69PmZM6g4yTX84TA==", "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -269,9 +269,9 @@
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
"integrity": "sha512-j3ucLdeY9HBcvODhCY4b+Ds3hWGO8t+SAidtmWu/ukfLLG/oYDMaA+dnugTVAg5fnUOGNbIYL9TOjhWgQB8W5g==", "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -285,9 +285,9 @@
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
"integrity": "sha512-uo5JL3cgaEGotaqSaJdRfFNSCUJOIliKLnDGWaVCgIKkHxwhYMm95pfMbWZ9l7GeW9kDg0tSxcy9NYdEtjwwmA==", "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -301,9 +301,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
"integrity": "sha512-DNdoRg8JX+gGsbqt2gPgkgb00mqOgOO27KnrWZtdABl6yWTST30aibGJ6geBq3WM2TIeW6COs5AScnC7GwtGPg==", "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -317,9 +317,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
"integrity": "sha512-aVsENlr7B64w8I1lhHShND5o8cW6sB9n9MUtLumFlPhG3elhNWtE7M1TFpj3m7lT3sKQUMkGFjTQBrvDDO1YWA==", "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -333,9 +333,9 @@
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
"integrity": "sha512-qbHGVQdKSwi0JQJuZznS4SyY27tYXYF0mrgthbxXrZI3AHKuRvU+Eqbg/F0rmLDpW/jkIZBlCO1XfHUBMNJ1pg==", "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -349,9 +349,9 @@
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
"integrity": "sha512-zsCp8Ql+96xXTVTmm6ffvoTSZSV2B/LzzkUXAY33F/76EajNw1m+jZ9zPfNJlJ3Rh4EzOszNDHsmG/fZOhtqDg==", "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -365,9 +365,9 @@
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
"integrity": "sha512-FfrFjR4id7wcFYOdqbDfDET3tjxCozUgbqdkOABsSFzoZGFC92UK7mg4JKRc/B3NNEf1s2WHxJ7VfTdVDPN3ng==", "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -381,9 +381,9 @@
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
"integrity": "sha512-JOOxw49BVZx2/5tW3FqkdjSD/5gXYeVGPDcB0lvap0gLQshkh1Nyel1QazC+wNxus3xPlsYAgqU1BUmrmCvWtw==", "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -477,9 +477,9 @@
} }
}, },
"node_modules/@tailwindcss/typography": { "node_modules/@tailwindcss/typography": {
"version": "0.5.9", "version": "0.5.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.9.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.10.tgz",
"integrity": "sha512-t8Sg3DyynFysV9f4JDOVISGsjazNb48AeIYQwcL+Bsq5uf4RYL75C1giZ43KISjeDGBaTN3Kxh7Xj/vRSMJUUg==", "integrity": "sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==",
"dependencies": { "dependencies": {
"lodash.castarray": "^4.4.0", "lodash.castarray": "^4.4.0",
"lodash.isplainobject": "^4.0.6", "lodash.isplainobject": "^4.0.6",
@@ -503,9 +503,9 @@
} }
}, },
"node_modules/@vitejs/plugin-vue": { "node_modules/@vitejs/plugin-vue": {
"version": "4.2.3", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.3.4.tgz",
"integrity": "sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==", "integrity": "sha512-ciXNIHKPriERBisHFBvnTbfKa6r9SAesOYXeGDzgegcvy9Q4xdScSHAmKbNT0M3O0S9LKhIf5/G+UYG4NnnzYw==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^14.18.0 || >=16.0.0" "node": "^14.18.0 || >=16.0.0"
@@ -683,9 +683,9 @@
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==" "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA=="
}, },
"node_modules/alpinejs": { "node_modules/alpinejs": {
"version": "3.12.2", "version": "3.13.0",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.12.2.tgz", "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.0.tgz",
"integrity": "sha512-wrUZULm9w6DYwWcUtB/anFLgRaF4riSuPgIJ3gcPUS8st9luAJnAxoIgro/Al97d2McSSz/JypWg/NlmKFIJJA==", "integrity": "sha512-7FYR1Yz3evIjlJD1mZ3SYWSw+jlOmQGeQ1QiSufSQ6J84XMQFkzxm6OobiZ928SfqhGdoIp2SsABNsS4rXMMJw==",
"dependencies": { "dependencies": {
"@vue/reactivity": "~3.1.1" "@vue/reactivity": "~3.1.1"
} }
@@ -719,9 +719,9 @@
"dev": true "dev": true
}, },
"node_modules/autoprefixer": { "node_modules/autoprefixer": {
"version": "10.4.14", "version": "10.4.16",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz",
"integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -731,12 +731,16 @@
{ {
"type": "tidelift", "type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/autoprefixer" "url": "https://tidelift.com/funding/github/npm/autoprefixer"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
} }
], ],
"dependencies": { "dependencies": {
"browserslist": "^4.21.5", "browserslist": "^4.21.10",
"caniuse-lite": "^1.0.30001464", "caniuse-lite": "^1.0.30001538",
"fraction.js": "^4.2.0", "fraction.js": "^4.3.6",
"normalize-range": "^0.1.2", "normalize-range": "^0.1.2",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"postcss-value-parser": "^4.2.0" "postcss-value-parser": "^4.2.0"
@@ -752,9 +756,9 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.4.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz",
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", "integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.0", "follow-redirects": "^1.15.0",
@@ -796,9 +800,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.21.5", "version": "4.21.11",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.11.tgz",
"integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", "integrity": "sha512-xn1UXOKUz7DjdGlg9RrUr0GGiWzI97UQJnugHtH0OLDfJB7jMgoIkYvRIEO1l9EeEERVqeqLYOcFBW9ldjypbQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -808,13 +812,17 @@
{ {
"type": "tidelift", "type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist" "url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
} }
], ],
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001449", "caniuse-lite": "^1.0.30001538",
"electron-to-chromium": "^1.4.284", "electron-to-chromium": "^1.4.526",
"node-releases": "^2.0.8", "node-releases": "^2.0.13",
"update-browserslist-db": "^1.0.10" "update-browserslist-db": "^1.0.13"
}, },
"bin": { "bin": {
"browserslist": "cli.js" "browserslist": "cli.js"
@@ -832,9 +840,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001467", "version": "1.0.30001539",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001467.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001539.tgz",
"integrity": "sha512-cEdN/5e+RPikvl9AHm4uuLXxeCNq8rFsQ+lPHTfe/OtypP3WwnVVbjn+6uBV7PaFL6xUFzTh+sSCOz1rKhcO+Q==", "integrity": "sha512-hfS5tE8bnNiNvEOEkm8HElUHroYwlqMMENEzELymy77+tJ6m+gA2krtHl5hxJaj71OlpC2cHZbdSMX1/YEqEkA==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -844,6 +852,10 @@
{ {
"type": "tidelift", "type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite" "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
} }
] ]
}, },
@@ -941,9 +953,9 @@
"dev": true "dev": true
}, },
"node_modules/daisyui": { "node_modules/daisyui": {
"version": "3.2.1", "version": "3.7.7",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-3.2.1.tgz", "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-3.7.7.tgz",
"integrity": "sha512-gIqE6wiqoJt9G8+n3R/SwLeUnpNCE2eDhT73rP0yZYVaM7o6zVcakBH3aEW5RGpx3UkonPiLuvcgxRcb2lE8TA==", "integrity": "sha512-2/nFdW/6R9MMnR8tTm07jPVyPaZwpUSkVsFAADb7Oq8N2Ynbls57laDdNqxTCUmn0QvcZi01TKl8zQbAwRfw1w==",
"dependencies": { "dependencies": {
"colord": "^2.9", "colord": "^2.9",
"css-selector-tokenizer": "^0.8", "css-selector-tokenizer": "^0.8",
@@ -979,15 +991,15 @@
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.4.332", "version": "1.4.528",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.332.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.528.tgz",
"integrity": "sha512-c1Vbv5tuUlBFp0mb3mCIjw+REEsgthRgNE8BlbEDKmvzb8rxjcVki6OkQP83vLN34s0XCxpSkq7AZNep1a6xhw==", "integrity": "sha512-UdREXMXzLkREF4jA8t89FQjA8WHI6ssP38PMY4/4KhXFQbtImnghh4GkCgrtiZwLKUKVD2iTVXvDVQjfomEQuA==",
"dev": true "dev": true
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.17.12", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.12.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
"integrity": "sha512-bX/zHl7Gn2CpQwcMtRogTTBf9l1nl+H6R8nUbjk+RuKqAE3+8FDulLA+pHvX7aA7Xe07Iwa+CWvy9I8Y2qqPKQ==", "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"bin": { "bin": {
@@ -997,28 +1009,28 @@
"node": ">=12" "node": ">=12"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/android-arm": "0.17.12", "@esbuild/android-arm": "0.18.20",
"@esbuild/android-arm64": "0.17.12", "@esbuild/android-arm64": "0.18.20",
"@esbuild/android-x64": "0.17.12", "@esbuild/android-x64": "0.18.20",
"@esbuild/darwin-arm64": "0.17.12", "@esbuild/darwin-arm64": "0.18.20",
"@esbuild/darwin-x64": "0.17.12", "@esbuild/darwin-x64": "0.18.20",
"@esbuild/freebsd-arm64": "0.17.12", "@esbuild/freebsd-arm64": "0.18.20",
"@esbuild/freebsd-x64": "0.17.12", "@esbuild/freebsd-x64": "0.18.20",
"@esbuild/linux-arm": "0.17.12", "@esbuild/linux-arm": "0.18.20",
"@esbuild/linux-arm64": "0.17.12", "@esbuild/linux-arm64": "0.18.20",
"@esbuild/linux-ia32": "0.17.12", "@esbuild/linux-ia32": "0.18.20",
"@esbuild/linux-loong64": "0.17.12", "@esbuild/linux-loong64": "0.18.20",
"@esbuild/linux-mips64el": "0.17.12", "@esbuild/linux-mips64el": "0.18.20",
"@esbuild/linux-ppc64": "0.17.12", "@esbuild/linux-ppc64": "0.18.20",
"@esbuild/linux-riscv64": "0.17.12", "@esbuild/linux-riscv64": "0.18.20",
"@esbuild/linux-s390x": "0.17.12", "@esbuild/linux-s390x": "0.18.20",
"@esbuild/linux-x64": "0.17.12", "@esbuild/linux-x64": "0.18.20",
"@esbuild/netbsd-x64": "0.17.12", "@esbuild/netbsd-x64": "0.18.20",
"@esbuild/openbsd-x64": "0.17.12", "@esbuild/openbsd-x64": "0.18.20",
"@esbuild/sunos-x64": "0.17.12", "@esbuild/sunos-x64": "0.18.20",
"@esbuild/win32-arm64": "0.17.12", "@esbuild/win32-arm64": "0.18.20",
"@esbuild/win32-ia32": "0.17.12", "@esbuild/win32-ia32": "0.18.20",
"@esbuild/win32-x64": "0.17.12" "@esbuild/win32-x64": "0.18.20"
} }
}, },
"node_modules/escalade": { "node_modules/escalade": {
@@ -1121,16 +1133,16 @@
} }
}, },
"node_modules/fraction.js": { "node_modules/fraction.js": {
"version": "4.2.0", "version": "4.3.6",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz",
"integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", "integrity": "sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "*" "node": "*"
}, },
"funding": { "funding": {
"type": "patreon", "type": "patreon",
"url": "https://www.patreon.com/infusion" "url": "https://github.com/sponsors/rawify"
} }
}, },
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
@@ -1277,9 +1289,9 @@
} }
}, },
"node_modules/laravel-vite-plugin": { "node_modules/laravel-vite-plugin": {
"version": "0.7.8", "version": "0.8.0",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-0.7.8.tgz", "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-0.8.0.tgz",
"integrity": "sha512-HWYqpQYHR3kEQ1LsHX7gHJoNNf0bz5z5mDaHBLzS+PGLCTmYqlU5/SZyeEgObV7z7bC/cnStYcY9H1DI1D5Udg==", "integrity": "sha512-6VjLI+azBpeK6rWBiKcb/En5GnTdYpL0U4zS8gXYvb2/VSq4mlau5H3NWpSktUDBMM1b97LLgICx5zevi8IY0w==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
@@ -1412,9 +1424,9 @@
} }
}, },
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.10", "version": "2.0.13",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
"integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
"dev": true "dev": true
}, },
"node_modules/normalize-path": { "node_modules/normalize-path": {
@@ -1504,9 +1516,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.24", "version": "8.4.30",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.30.tgz",
"integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", "integrity": "sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -1697,9 +1709,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "3.21.0", "version": "3.29.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.0.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.3.tgz",
"integrity": "sha512-ANPhVcyeHvYdQMUyCbczy33nbLzI7RzrBje4uvNiTDJGIMtlKoOStmympwr9OtS1LZxiDmE2wvxHyVhoLtf1KQ==", "integrity": "sha512-T7du6Hum8jOkSWetjRgbwpM6Sy0nECYrYRSmZjayFcOddtKJWU4d17AC3HNUk7HRuqy4p+G7aEZclSHytqUmEg==",
"dev": true, "dev": true,
"bin": { "bin": {
"rollup": "dist/bin/rollup" "rollup": "dist/bin/rollup"
@@ -1794,9 +1806,9 @@
} }
}, },
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "3.3.2", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
"integrity": "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==", "integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==",
"dependencies": { "dependencies": {
"@alloc/quick-lru": "^5.2.0", "@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2", "arg": "^5.0.2",
@@ -1818,7 +1830,6 @@
"postcss-load-config": "^4.0.1", "postcss-load-config": "^4.0.1",
"postcss-nested": "^6.0.1", "postcss-nested": "^6.0.1",
"postcss-selector-parser": "^6.0.11", "postcss-selector-parser": "^6.0.11",
"postcss-value-parser": "^4.2.0",
"resolve": "^1.22.2", "resolve": "^1.22.2",
"sucrase": "^3.32.0" "sucrase": "^3.32.0"
}, },
@@ -1874,9 +1885,9 @@
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
}, },
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.0.10", "version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
"integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -1886,6 +1897,10 @@
{ {
"type": "tidelift", "type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist" "url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
} }
], ],
"dependencies": { "dependencies": {
@@ -1893,7 +1908,7 @@
"picocolors": "^1.0.0" "picocolors": "^1.0.0"
}, },
"bin": { "bin": {
"browserslist-lint": "cli.js" "update-browserslist-db": "cli.js"
}, },
"peerDependencies": { "peerDependencies": {
"browserslist": ">= 4.21.0" "browserslist": ">= 4.21.0"
@@ -1905,14 +1920,14 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "4.3.9", "version": "4.4.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz",
"integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==", "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "^0.17.5", "esbuild": "^0.18.10",
"postcss": "^8.4.23", "postcss": "^8.4.27",
"rollup": "^3.21.0" "rollup": "^3.27.1"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
@@ -1920,12 +1935,16 @@
"engines": { "engines": {
"node": "^14.18.0 || >=16.0.0" "node": "^14.18.0 || >=16.0.0"
}, },
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": { "optionalDependencies": {
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
}, },
"peerDependencies": { "peerDependencies": {
"@types/node": ">= 14", "@types/node": ">= 14",
"less": "*", "less": "*",
"lightningcss": "^1.21.0",
"sass": "*", "sass": "*",
"stylus": "*", "stylus": "*",
"sugarss": "*", "sugarss": "*",
@@ -1938,6 +1957,9 @@
"less": { "less": {
"optional": true "optional": true
}, },
"lightningcss": {
"optional": true
},
"sass": { "sass": {
"optional": true "optional": true
}, },

View File

@@ -6,19 +6,19 @@
"build": "vite build" "build": "vite build"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "4.2.3", "@vitejs/plugin-vue": "4.3.4",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.16",
"axios": "1.4.0", "axios": "1.5.0",
"laravel-vite-plugin": "0.7.8", "laravel-vite-plugin": "0.8.0",
"postcss": "8.4.24", "postcss": "8.4.30",
"tailwindcss": "3.3.2", "tailwindcss": "3.3.3",
"vite": "4.3.9", "vite": "4.4.9",
"vue": "3.3.4" "vue": "3.3.4"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/typography": "0.5.9", "@tailwindcss/typography": "0.5.10",
"alpinejs": "3.12.2", "alpinejs": "3.13.0",
"daisyui": "3.2.1", "daisyui": "3.7.7",
"tailwindcss-scrollbar": "0.1.0" "tailwindcss-scrollbar": "0.1.0"
} }
} }

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
{ {
"/app.js": "/app.js?id=7e1968acfd75b8dc843675097962e3ce", "/app.js": "/app.js?id=ff1533ec4a7afad65c5bd7bcc2cc7d7b",
"/app-dark.css": "/app-dark.css?id=15c72df05e2b1147fa3e4b0670cfb435", "/app-dark.css": "/app-dark.css?id=15c72df05e2b1147fa3e4b0670cfb435",
"/app.css": "/app.css?id=4d6a1a7fe095eedc2cb2a4ce822ea8a5", "/app.css": "/app.css?id=4d6a1a7fe095eedc2cb2a4ce822ea8a5",
"/img/favicon.png": "/img/favicon.png?id=1542bfe8a0010dcbee710da13cce367f", "/img/favicon.png": "/img/favicon.png?id=1542bfe8a0010dcbee710da13cce367f",

View File

@@ -1,7 +1,7 @@
<template> <template>
<Transition name="fade"> <Transition name="fade">
<div> <div >
<div class="flex items-center p-1 px-2 mt-1 overflow-hidden transition-all transform rounded cursor-pointer bg-coolgray-200" <div class="flex items-center p-1 px-2 overflow-hidden transition-all transform rounded cursor-pointer bg-coolgray-200"
@click="showCommandPalette = true"> @click="showCommandPalette = true">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 icon" viewBox="0 0 24 24" stroke-width="2" <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 icon" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">

View File

@@ -15,14 +15,13 @@
@if (is_transactional_emails_active()) @if (is_transactional_emails_active())
<form action="/forgot-password" method="POST" class="flex flex-col gap-2"> <form action="/forgot-password" method="POST" class="flex flex-col gap-2">
@csrf @csrf
<x-forms.input required type="email" name="email" <x-forms.input required type="email" name="email" label="{{ __('input.email') }}" autofocus />
label="{{ __('input.email') }}" autofocus />
<x-forms.button type="submit">{{ __('auth.forgot_password_send_email') }}</x-forms.button> <x-forms.button type="submit">{{ __('auth.forgot_password_send_email') }}</x-forms.button>
</form> </form>
@else @else
<div>Transactional emails are not active on this instance.</div> <div>Transactional emails are not active on this instance.</div>
<div>See how to set it in our <a class="text-white" target="_blank" <div>See how to set it in our <a class="text-white" target="_blank"
href="https://docs.coollabs.io/coolify">docs</a>, or how to href="{{ config('constants.docs.base_url') }}">docs</a>, or how to
manually reset password. manually reset password.
</div> </div>
@endif @endif

View File

@@ -19,7 +19,7 @@
@foreach (Str::of(data_get($application, 'fqdn'))->explode(',') as $fqdn) @foreach (Str::of(data_get($application, 'fqdn'))->explode(',') as $fqdn)
<li> <li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white" <a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank" href="{{ getOnlyFqdn($fqdn) }}"> target="_blank" href="{{ getFqdnWithoutPort($fqdn) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round"> stroke-linejoin="round">
@@ -28,7 +28,7 @@
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" /> <path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path <path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" /> d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>{{ getOnlyFqdn($fqdn) }} </svg>{{ getFqdnWithoutPort($fqdn) }}
</a> </a>
</li> </li>
@endforeach @endforeach
@@ -38,7 +38,7 @@
@if (data_get($preview, 'fqdn')) @if (data_get($preview, 'fqdn'))
<li> <li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white" <a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank" href="{{ getOnlyFqdn(data_get($preview, 'fqdn')) }}"> target="_blank" href="{{ getFqdnWithoutPort(data_get($preview, 'fqdn')) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round"> stroke-linejoin="round">

View File

@@ -0,0 +1,12 @@
@isset($title, $action)
<div tabindex="0" x-data="{ open: false }"
class="transition border rounded cursor-pointer collapse collapse-arrow border-coolgray-200"
:class="open ? 'collapse-open' : 'collapse-close'">
<div class="flex flex-col justify-center text-sm collapse-title" x-on:click="open = !open">
{{ $title }}
</div>
<div class="collapse-content">
{{ $action }}
</div>
</div>
@endisset

View File

@@ -1,5 +1,6 @@
@auth @auth
<nav class="fixed h-full overflow-hidden overflow-y-auto scrollbar"> <nav class="fixed h-full overflow-hidden overflow-y-auto pt-14 scrollbar">
<a href="/" class="fixed top-0 z-50 mx-3 mt-3 cursor-pointer bg-coolgray-100"><img class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a>
<ul class="flex flex-col h-full gap-4 menu flex-nowrap"> <ul class="flex flex-col h-full gap-4 menu flex-nowrap">
<li title="Dashboard"> <li title="Dashboard">
<a class="hover:bg-transparent" @if (!request()->is('/')) href="/" @endif> <a class="hover:bg-transparent" @if (!request()->is('/')) href="/" @endif>
@@ -10,16 +11,13 @@
</svg> </svg>
</a> </a>
</li> </li>
<li title="Help" class="mt-auto"> <li title="Send us feedback or get help!" class="fixed top-0 right-0 p-2 px-4 pt-4 mt-auto text-xs">
<div class="justify-center icons" wire:click="help" onclick="help.showModal()"> <div class="justify-center" wire:click="help" onclick="help.showModal()">
<svg class="{{ request()->is('help*') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24" <svg class="w-5 h-5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
xmlns="http://www.w3.org/2000/svg"> <path fill="currentColor"
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M22 5.5H9c-1.1 0-2 .9-2 2v9a2 2 0 0 0 2 2h13c1.11 0 2-.89 2-2v-9a2 2 0 0 0-2-2m0 11H9V9.17l6.5 3.33L22 9.17v7.33m-6.5-5.69L9 7.5h13l-6.5 3.31M5 16.5c0 .17.03.33.05.5H1c-.552 0-1-.45-1-1s.448-1 1-1h4v1.5M3 7h2.05c-.02.17-.05.33-.05.5V9H3c-.55 0-1-.45-1-1s.45-1 1-1m-2 5c0-.55.45-1 1-1h3v2H2c-.55 0-1-.45-1-1Z" />
stroke-width="2">
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0-18 0m9 4v.01" />
<path d="M12 13a2 2 0 0 0 .914-3.782a1.98 1.98 0 0 0-2.414.483" />
</g>
</svg> </svg>
Feedback
</div> </div>
</li> </li>
<li class="pb-6" title="Logout"> <li class="pb-6" title="Logout">

View File

@@ -1,5 +1,6 @@
@auth @auth
<nav class="fixed h-full overflow-hidden overflow-y-auto pt-14 scrollbar"> <nav class="fixed h-full overflow-hidden overflow-y-auto pt-28 scrollbar">
<a href="/" class="fixed top-0 z-50 mx-3 mt-3 cursor-pointer bg-coolgray-100"><img class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a>
<ul class="flex flex-col h-full gap-4 menu flex-nowrap"> <ul class="flex flex-col h-full gap-4 menu flex-nowrap">
<li title="Dashboard"> <li title="Dashboard">
<a class="hover:bg-transparent" @if (!request()->is('/')) href="/" @endif> <a class="hover:bg-transparent" @if (!request()->is('/')) href="/" @endif>
@@ -114,16 +115,13 @@
</li> </li>
@endif @endif
@if (isSubscriptionActive() || isDev()) @if (isSubscriptionActive() || isDev())
<li title="Help" class="mt-auto"> <li title="Send us feedback or get help!" class="fixed top-0 right-0 p-2 px-4 pt-4 mt-auto text-xs">
<div class="justify-center icons" wire:click="help" onclick="help.showModal()"> <div class="justify-center" wire:click="help" onclick="help.showModal()">
<svg class="{{ request()->is('help*') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24" <svg class="w-5 h-5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
xmlns="http://www.w3.org/2000/svg"> <path fill="currentColor"
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M22 5.5H9c-1.1 0-2 .9-2 2v9a2 2 0 0 0 2 2h13c1.11 0 2-.89 2-2v-9a2 2 0 0 0-2-2m0 11H9V9.17l6.5 3.33L22 9.17v7.33m-6.5-5.69L9 7.5h13l-6.5 3.31M5 16.5c0 .17.03.33.05.5H1c-.552 0-1-.45-1-1s.448-1 1-1h4v1.5M3 7h2.05c-.02.17-.05.33-.05.5V9H3c-.55 0-1-.45-1-1s.45-1 1-1m-2 5c0-.55.45-1 1-1h3v2H2c-.55 0-1-.45-1-1Z" />
stroke-width="2">
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0-18 0m9 4v.01" />
<path d="M12 13a2 2 0 0 0 .914-3.782a1.98 1.98 0 0 0-2.414.483" />
</g>
</svg> </svg>
Feedback
</div> </div>
</li> </li>
@endif @endif

View File

@@ -1,28 +1,31 @@
<div class="group"> @if ($links->count() > 0)
<label tabindex="0" class="flex items-center gap-2 cursor-pointer hover:text-white"> Open Application <div class="group">
<x-chevron-down /> <label tabindex="0" class="flex items-center gap-2 cursor-pointer hover:text-white"> Open Application
</label> <x-chevron-down />
</label>
<div class="absolute hidden group-hover:block"> <div class="absolute hidden group-hover:block">
<ul tabindex="0" class="relative -ml-24 text-xs text-white normal-case rounded min-w-max menu bg-coolgray-200"> <ul tabindex="0"
@if ($links->count() > 0) class="relative -ml-24 text-xs text-white normal-case rounded min-w-max menu bg-coolgray-200">
@foreach ($links as $link) @if ($links->count() > 0)
<li> @foreach ($links as $link)
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white" <li>
target="_blank" href="{{ $link }}"> <a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" target="_blank" href="{{ $link }}">
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-linejoin="round"> stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> stroke-linejoin="round">
<path d="M9 15l6 -6" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" /> <path d="M9 15l6 -6" />
<path <path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" /> <path
</svg>{{ $link }} d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</a> </svg>{{ $link }}
</li> </a>
@endforeach </li>
@endif @endforeach
</ul> @endif
</ul>
</div>
</div> </div>
</div> @endif

View File

@@ -1,27 +1,7 @@
<div class="navbar-main"> <div class="navbar-main">
<x-services.links :service="$service" /> <x-services.links :service="$service" />
<div class="flex-1"></div> <div class="flex-1"></div>
@if (serviceStatus($service) === 'running') @if (serviceStatus($service) === 'degraded')
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
</svg>
Stop
</button>
@elseif(serviceStatus($service) === 'exited')
<button wire:click='deploy' onclick="startService.showModal()"
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</button>
@elseif (serviceStatus($service) === 'degraded')
<button wire:click='deploy' onclick="startService.showModal()" <button wire:click='deploy' onclick="startService.showModal()"
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@@ -42,4 +22,34 @@
Stop Stop
</button> </button>
@endif @endif
@if (serviceStatus($service) === 'running')
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
</svg>
Stop
</button>
@endif
@if (serviceStatus($service) === 'exited')
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 " viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path fill="red" d="M26 20h-6v-2h6zm4 8h-6v-2h6zm-2-4h-6v-2h6z" />
<path fill="red"
d="M17.003 20a4.895 4.895 0 0 0-2.404-4.173L22 3l-1.73-1l-7.577 13.126a5.699 5.699 0 0 0-5.243 1.503C3.706 20.24 3.996 28.682 4.01 29.04a1 1 0 0 0 1 .96h14.991a1 1 0 0 0 .6-1.8c-3.54-2.656-3.598-8.146-3.598-8.2Zm-5.073-3.003A3.11 3.11 0 0 1 15.004 20c0 .038.002.208.017.469l-5.9-2.624a3.8 3.8 0 0 1 2.809-.848ZM15.45 28A5.2 5.2 0 0 1 14 25h-2a6.5 6.5 0 0 0 .968 3h-2.223A16.617 16.617 0 0 1 10 24H8a17.342 17.342 0 0 0 .665 4H6c.031-1.836.29-5.892 1.803-8.553l7.533 3.35A13.025 13.025 0 0 0 17.596 28Z" />
</svg>
Force cleanup containers
</button>
<button wire:click='deploy' onclick="startService.showModal()"
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</button>
@endif
</div> </div>

View File

@@ -1,2 +1,2 @@
<a {{ $attributes->merge(['class' => 'text-xs cursor-pointer opacity-20 hover:opacity-100 hover:text-white']) }} <a {{ $attributes->merge(['class' => 'text-xs cursor-pointer opacity-20 hover:opacity-100 hover:text-white z-50']) }}
href="https://github.com/coollabsio/coolify/releases/tag/v{{ config('version') }}">v{{ config('version') }}</a> href="https://github.com/coollabsio/coolify/releases/tag/v{{ config('version') }}">v{{ config('version') }}</a>

View File

@@ -0,0 +1,5 @@
<x-emails.layout>
<br><br>
If you do not like to receive these emails, you can unsubscribe [here]({{$unsubscribeUrl}}).
</x-emails.layout>

View File

@@ -7,8 +7,8 @@
<p class="mt-6 text-base leading-7 text-neutral-300">There has been an error, we are working on it. <p class="mt-6 text-base leading-7 text-neutral-300">There has been an error, we are working on it.
</p> </p>
@if ($exception->getMessage() !== '') @if ($exception->getMessage() !== '')
<p class="mt-6 text-xs leading-7 text-left text-red-500">Error: {{ $exception->getMessage() }} <code class="mt-6 text-xs text-left text-red-500">Error: {{ $exception->getMessage() }}
</p> </code>
@endif @endif
<div class="flex items-center justify-center mt-10 gap-x-6"> <div class="flex items-center justify-center mt-10 gap-x-6">
<a href="/"> <a href="/">

View File

@@ -2,7 +2,7 @@
@section('body') @section('body')
@parent @parent
<x-navbar /> <x-navbar />
<div class="fixed top-3 left-4" id="vue"> <div class="fixed z-50 top-[4.5rem] left-4" id="vue">
<magic-bar></magic-bar> <magic-bar></magic-bar>
</div> </div>
<main class="main max-w-screen-2xl"> <main class="main max-w-screen-2xl">

View File

@@ -94,6 +94,7 @@
}) })
}, 2000); }, 2000);
} }
function copyToClipboard(text) { function copyToClipboard(text) {
navigator.clipboard.writeText(text); navigator.clipboard.writeText(text);
Livewire.emit('success', 'Copied to clipboard.'); Livewire.emit('success', 'Copied to clipboard.');

View File

@@ -2,7 +2,7 @@
@section('body') @section('body')
@parent @parent
@if (isSubscriptionOnGracePeriod()) @if (isSubscriptionOnGracePeriod())
<div class="fixed top-3 left-4" id="vue"> <div class="fixed top-[4.5rem] left-4 z-50" id="vue">
<magic-bar></magic-bar> <magic-bar></magic-bar>
</div> </div>
<x-navbar /> <x-navbar />

View File

@@ -10,7 +10,7 @@
</div> </div>
@endif @endif
<div <div
class="scrollbar flex flex-col-reverse w-full overflow-y-auto border border-solid rounded border-coolgray-300 max-h-[32rem] p-4 text-xs text-white"> class="scrollbar flex flex-col-reverse w-full overflow-y-auto border border-solid rounded border-coolgray-300 max-h-[32rem] p-4 pt-6 text-xs text-white">
<pre class="font-mono whitespace-pre-wrap" @if ($isPollingActive) wire:poll.2000ms="polling" @endif>{{ RunRemoteProcess::decodeOutput($this->activity) }}</pre> <pre class="font-mono whitespace-pre-wrap" @if ($isPollingActive) wire:poll.2000ms="polling" @endif>{{ RunRemoteProcess::decodeOutput($this->activity) }}</pre>
</div> </div>

View File

@@ -40,7 +40,7 @@
<x-boarding-step title="Server"> <x-boarding-step title="Server">
<x-slot:question> <x-slot:question>
Do you want to deploy your resources on your <x-highlighted text="Localhost" /> Do you want to deploy your resources on your <x-highlighted text="Localhost" />
or on a <x-highlighted text="Remote Server" />? or on a<x-highlighted text="Remote Server" />?
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center box" wire:target="setServerType('localhost')" <x-forms.button class="justify-center box" wire:target="setServerType('localhost')"

View File

@@ -1,10 +1,11 @@
<div class="flex flex-col gap-2 rounded modal-box"> <div class="flex flex-col w-11/12 max-w-5xl gap-2 modal-box">
<h3>How can we help?</h3> <h3>How can we help?</h3>
<div>You can report bug about the current page (details will be included automatically), or send us general feedback.</div> <div>Your feedback helps us to improve Coolify. Thank you! 💜</div>
<form wire:submit.prevent="submit" class="flex flex-col gap-4 pt-4"> <form wire:submit.prevent="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 id="subject" label="Subject" placeholder="Summary of your problem."></x-forms.input>
<x-forms.textarea id="description" label="Message" <x-forms.textarea rows="10" id="description" label="Description"
placeholder="Please provide as much information as possible."></x-forms.textarea> placeholder="Please provide as much information as possible."></x-forms.textarea>
<x-forms.button class="w-full mt-4" type="submit">Send Request</x-forms.button> <div></div>
<x-forms.button class="w-full mt-4" type="submit" onclick="help.close()">Send Email</x-forms.button>
</form> </form>
</div> </div>

View File

@@ -26,6 +26,14 @@
@endif @endif
@endif @endif
</div> </div>
@if (!$application->dockerfile)
<div class="flex items-end gap-2">
<x-forms.select id="application.build_pack" label="Build Pack" required>
<option value="nixpacks">Nixpacks</option>
<option value="dockerfile">Dockerfile</option>
</x-forms.select>
</div>
@endif
@if ($application->settings->is_static) @if ($application->settings->is_static)
<x-forms.select id="application.static_image" label="Static Image" required> <x-forms.select id="application.static_image" label="Static Image" required>
<option value="nginx:alpine">nginx:alpine</option> <option value="nginx:alpine">nginx:alpine</option>
@@ -54,6 +62,7 @@
@if ($application->dockerfile) @if ($application->dockerfile)
<x-forms.textarea label="Dockerfile" id="application.dockerfile" rows="6"> </x-forms.textarea> <x-forms.textarea label="Dockerfile" id="application.dockerfile" rows="6"> </x-forms.textarea>
@endif @endif
<h3>Network</h3> <h3>Network</h3>
<div class="flex flex-col gap-2 xl:flex-row"> <div class="flex flex-col gap-2 xl:flex-row">
@if ($application->settings->is_static) @if ($application->settings->is_static)
@@ -63,7 +72,7 @@
helper="A comma separated list of ports your application uses. The first port will be used as default healthcheck port if nothing defined in the Healthcheck menu. Be sure to set this correctly." /> helper="A comma separated list of ports your application uses. The first port will be used as default healthcheck port if nothing defined in the Healthcheck menu. Be sure to set this correctly." />
@endif @endif
<x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings" <x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><span class='inline-block font-bold text-warning'>Example</span>3000:3000,3002:3002" /> helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host." />
</div> </div>
</div> </div>
<h3>Advanced</h3> <h3>Advanced</h3>

View File

@@ -12,9 +12,10 @@
<br> <br>
- SERVICE_FQDN_*: FQDN - could be changable from the UI. (example: SERVICE_FQDN_GHOST)<br> - SERVICE_FQDN_*: FQDN - could be changable from the UI. (example: SERVICE_FQDN_GHOST)<br>
- SERVICE_URL_*: URL parsed from FQDN - could be changable from the UI. (example: SERVICE_URL_GHOST)<br> - SERVICE_URL_*: URL parsed from FQDN - could be changable from the UI. (example: SERVICE_URL_GHOST)<br>
- SERVICE_USER_*: Generated user, not encrypted in database (example: SERVICE_USER_MYSQL)<br> - SERVICE_BASE64_64_*: Generated 'base64' string with length of '64' (example: SERVICE_BASE64_64_GHOST, to generate 32 bit: SERVICE_BASE64_32_GHOST)<br>
- SERVICE_PASSWORD_*: Generated password, encrypted in database (example: SERVICE_PASSWORD_MYSQL)<br>" - SERVICE_USER_*: Generated user (example: SERVICE_USER_MYSQL)<br>
rows="20" id="dockercompose" - SERVICE_PASSWORD_*: Generated password (example: SERVICE_PASSWORD_MYSQL)<br>"
rows="20" id="dockerComposeRaw"
placeholder='services: placeholder='services:
ghost: ghost:
documentation: https://ghost.org/docs/config documentation: https://ghost.org/docs/config
@@ -43,5 +44,6 @@
- MYSQL_DATABASE=${MYSQL_DATABASE} - MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQL_ROOT} - MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQL_ROOT}
'></x-forms.textarea> '></x-forms.textarea>
{{-- <x-forms.textarea label="Environment File" rows="20" id="envFile"></x-forms.textarea> --}}
</form> </form>
</div> </div>

View File

@@ -1,4 +1,4 @@
<div x-data x-init="$wire.load_servers"> <div x-data x-init="$wire.loadThings">
<h1>New Resource</h1> <h1>New Resource</h1>
<div class="pb-4 ">Deploy resources, like Applications, Databases, Services...</div> <div class="pb-4 ">Deploy resources, like Applications, Databases, Services...</div>
<div class="flex flex-col gap-2 pt-10"> <div class="flex flex-col gap-2 pt-10">
@@ -12,7 +12,7 @@
<div class="grid justify-start grid-cols-1 gap-2 text-left xl:grid-cols-3"> <div class="grid justify-start grid-cols-1 gap-2 text-left xl:grid-cols-3">
<div class="box group" wire:click="setType('public')"> <div class="box group" wire:click="setType('public')">
<div class="flex flex-col mx-6"> <div class="flex flex-col mx-6">
<div class="group-hover:text-white"> <div class="font-bold text-white group-hover:text-white">
Public Repository Public Repository
</div> </div>
<div class="text-xs group-hover:text-white"> <div class="text-xs group-hover:text-white">
@@ -22,7 +22,7 @@
</div> </div>
<div class="box group" wire:click="setType('private-gh-app')"> <div class="box group" wire:click="setType('private-gh-app')">
<div class="flex flex-col mx-6"> <div class="flex flex-col mx-6">
<div class="group-hover:text-white"> <div class="font-bold text-white group-hover:text-white">
Private Repository Private Repository
</div> </div>
<div class="text-xs group-hover:text-white"> <div class="text-xs group-hover:text-white">
@@ -32,7 +32,7 @@
</div> </div>
<div class="box group" wire:click="setType('private-deploy-key')"> <div class="box group" wire:click="setType('private-deploy-key')">
<div class="flex flex-col mx-6"> <div class="flex flex-col mx-6">
<div class="group-hover:text-white"> <div class="font-bold text-white group-hover:text-white">
Private Repository (with deploy key) Private Repository (with deploy key)
</div> </div>
<div class="text-xs group-hover:text-white"> <div class="text-xs group-hover:text-white">
@@ -44,7 +44,7 @@
<div class="grid justify-start grid-cols-1 gap-2 text-left xl:grid-cols-3"> <div class="grid justify-start grid-cols-1 gap-2 text-left xl:grid-cols-3">
<div class="box group" wire:click="setType('dockerfile')"> <div class="box group" wire:click="setType('dockerfile')">
<div class="flex flex-col mx-6"> <div class="flex flex-col mx-6">
<div class="group-hover:text-white"> <div class="font-bold text-white group-hover:text-white">
Based on a Dockerfile Based on a Dockerfile
</div> </div>
<div class="text-xs group-hover:text-white"> <div class="text-xs group-hover:text-white">
@@ -52,24 +52,22 @@
</div> </div>
</div> </div>
</div> </div>
@if (isDev()) <div class="box group" wire:click="setType('docker-compose-empty')">
<div class="box group" wire:click="setType('dockercompose')"> <div class="flex flex-col mx-6">
<div class="flex flex-col mx-6"> <div class="font-bold text-white group-hover:text-white">
<div class="group-hover:text-white"> Based on a Docker Compose
Based on a Docker Compose </div>
</div> <div class="text-xs group-hover:text-white">
<div class="text-xs group-hover:text-white"> You can deploy complex application easily with Docker Compose.
You can deploy complex application easily with Docker Compose.
</div>
</div> </div>
</div> </div>
@endif </div>
</div> </div>
<h2 class="py-4">Databases</h2> <h2 class="py-4">Databases</h2>
<div class="grid justify-start grid-cols-1 gap-2 text-left xl:grid-cols-3"> <div class="grid justify-start grid-cols-1 gap-2 text-left xl:grid-cols-3">
<div class="box group" wire:click="setType('postgresql')"> <div class="box group" wire:click="setType('postgresql')">
<div class="flex flex-col mx-6"> <div class="flex flex-col mx-6">
<div class="group-hover:text-white"> <div class="font-bold text-white group-hover:text-white">
New PostgreSQL New PostgreSQL
</div> </div>
<div class="text-xs group-hover:text-white"> <div class="text-xs group-hover:text-white">
@@ -88,9 +86,30 @@
</div> </div>
</div> --}} </div> --}}
</div> </div>
<h2 class="py-4">Services</h2> <div class="flex items-center gap-2">
<h2 class="py-4">Services</h2>
<x-forms.button wire:click='loadServices(true)'>Reload Services List</x-forms.button>
</div>
<div class="grid justify-start grid-cols-1 gap-2 text-left xl:grid-cols-3"> <div class="grid justify-start grid-cols-1 gap-2 text-left xl:grid-cols-3">
Ghost, Plausible, Wordpress, etc... Coming very very soon... @if ($loadingServices)
<span class="loading loading-xs loading-spinner"></span>
@else
@foreach ($services as $serviceName => $service)
<button class="text-left box group"
wire:loading.attr="disabled" wire:click="setType('one-click-service-{{ $serviceName }}')">
<div class="flex flex-col mx-6">
<div class="font-bold text-white group-hover:text-white">
{{ Str::headline($serviceName) }}
</div>
@if (data_get($service, 'slogan'))
<div class="text-xs">
{{ data_get($service, 'slogan') }}
</div>
@endif
</div>
</button>
@endforeach
@endif
</div> </div>
@endif @endif
@if ($current_step === 'servers') @if ($current_step === 'servers')

View File

@@ -7,21 +7,33 @@
<h2>{{ Str::headline($application->name) }}</h2> <h2>{{ Str::headline($application->name) }}</h2>
@endif @endif
<x-forms.button type="submit">Save</x-forms.button> <x-forms.button type="submit">Save</x-forms.button>
<a target="_blank" href="{{ $application->documentation() }}">Documentation <x-external-link /></a> <x-forms.button isError wire:click='delete'>Delete</x-forms.button>
</div> </div>
<div class="flex gap-2"> <div class="flex flex-col gap-2">
<x-forms.input label="Name" id="application.human_name" placeholder="Human readable name"></x-forms.input> <div class="flex gap-2">
<x-forms.input label="Description" id="application.description"></x-forms.input> <x-forms.input label="Name" id="application.human_name"
<x-forms.input placeholder="https://app.coolify.io" label="Domains" required placeholder="Human readable name"></x-forms.input>
id="application.fqdn"></x-forms.input> <x-forms.input label="Description" id="application.description"></x-forms.input>
</div>
<div class="flex gap-2">
@if ($application->required_fqdn)
<x-forms.input required placeholder="https://app.coolify.io" label="Domains"
id="application.fqdn"></x-forms.input>
@else
<x-forms.input placeholder="https://app.coolify.io" label="Domains"
id="application.fqdn"></x-forms.input>
@endif
<x-forms.input required
helper="You can change the image you would like to deploy.<br><br><span class='text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"
label="Image" id="application.image"></x-forms.input>
</div>
</div>
<h3 class="pt-2">Advanced</h3>
<div class="w-64">
<x-forms.checkbox instantSave label="Exclude from service status"
helper="If you do not need to monitor this resource, enable. Useful if this service is optional."
id="application.exclude_from_status"></x-forms.checkbox>
</div> </div>
</form> </form>
@if ($application->fileStorages()->get()->count() > 0)
<h3 class="py-4">File Storages</h3>
<div class="flex flex-col gap-4">
@foreach ($application->fileStorages()->get() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="{{ $loop->index }}" />
@endforeach
</div>
@endif
</div> </div>

View File

@@ -1,15 +1,28 @@
<form wire:submit.prevent='submit'> <div>
<div class="flex items-center gap-2 pb-4"> <form wire:submit.prevent='submit'>
@if ($database->human_name) <div class="flex items-center gap-2 pb-4">
<h2>{{ Str::headline($database->human_name) }}</h2> @if ($database->human_name)
@else <h2>{{ Str::headline($database->human_name) }}</h2>
<h2>{{ Str::headline($database->name) }}</h2> @else
@endif <h2>{{ Str::headline($database->name) }}</h2>
<x-forms.button type="submit">Save</x-forms.button> @endif
<a target="_blank" href="{{ $database->documentation() }}">Documentation <x-external-link /></a> <x-forms.button type="submit">Save</x-forms.button>
</div> </div>
<div class="flex gap-2"> <div class="flex flex-col gap-2">
<x-forms.input label="Name" id="database.human_name" placeholder="Name"></x-forms.input> <div class="flex gap-2">
<x-forms.input label="Description" id="database.description"></x-forms.input> <x-forms.input label="Name" id="database.human_name" placeholder="Name"></x-forms.input>
</div> <x-forms.input label="Description" id="database.description"></x-forms.input>
</form> </div>
<div class="flex gap-2">
<x-forms.input required helper="You can change the image you would like to deploy.<br><br><span class='text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>" label="Image Tag"
id="database.image"></x-forms.input>
</div>
</div>
<h3 class="pt-2">Advanced</h3>
<div class="w-64">
<x-forms.checkbox instantSave label="Exclude from service status"
helper="If you do not need to monitor this resource, enable. Useful if this service is optional."
id="database.exclude_from_status"></x-forms.checkbox>
</div>
</form>
</div>

View File

@@ -1,8 +1,23 @@
<form wire:submit.prevent='submit' class="flex flex-col gap-2"> <x-collapsible>
<div class="flex gap-2"> <x-slot:title>
<x-forms.input readonly label="File Path" id="fileStorage.fs_path"></x-forms.input> <div>{{ $fileStorage->mount_path }} </div>
<x-forms.input readonly label="Mount Path (in container)" id="fileStorage.mount_path"></x-forms.input> </x-slot:title>
</div> <x-slot:action>
<x-forms.textarea label="Content" id="fileStorage.content"></x-forms.textarea> <form wire:submit.prevent='submit' class="flex flex-col gap-2">
<x-forms.button type="submit">Save</x-forms.button> <div class="w-64">
</form> <x-forms.checkbox instantSave label="Is directory?" id="fileStorage.is_directory"></x-forms.checkbox>
</div>
@if ($fileStorage->is_directory)
<x-forms.input readonly label="Directory on Filesystem (save files here)" id="fs_path"></x-forms.input>
@else
<div class="flex gap-2">
<x-forms.input readonly label="File in Docker Compose file" id="fileStorage.fs_path"></x-forms.input>
<x-forms.input readonly label="File on Filesystem (save files here)" id="fs_path"></x-forms.input>
</div>
<x-forms.input readonly label="Mount (in container)" id="fileStorage.mount_path"></x-forms.input>
<x-forms.textarea label="Content" rows="20" id="fileStorage.content"></x-forms.textarea>
<x-forms.button type="submit">Save</x-forms.button>
@endif
</form>
</x-slot:action>
</x-collapsible>

View File

@@ -1,12 +1,11 @@
<div x-data="{ raw: true, activeTab: window.location.hash ? window.location.hash.substring(1) : 'service-stack' }"> <div x-data="{ raw: true, activeTab: window.location.hash ? window.location.hash.substring(1) : 'service-stack' }" wire:poll.10000ms="checkStatus">
<livewire:project.service.navbar :service="$service" :parameters="$parameters" :query="$query" /> <livewire:project.service.navbar :service="$service" :parameters="$parameters" :query="$query" />
<div class="flex h-full pt-6"> <div class="flex h-full pt-6">
<div class="flex flex-col gap-4 min-w-fit"> <div class="flex flex-col items-start gap-4 min-w-fit">
<a target="_blank" href="{{ $service->documentation() }}">Documentation <x-external-link /></a>
<a :class="activeTab === 'service-stack' && 'text-white'" <a :class="activeTab === 'service-stack' && 'text-white'"
@click.prevent="activeTab = 'service-stack'; window.location.hash = 'service-stack'" @click.prevent="activeTab = 'service-stack'; window.location.hash = 'service-stack'"
href="#">Service Stack</a> href="#">Service Stack</a>
<a :class="activeTab === 'compose' && 'text-white'"
@click.prevent="activeTab = 'compose'; window.location.hash = 'compose'" href="#">Compose File</a>
<a :class="activeTab === 'environment-variables' && 'text-white'" <a :class="activeTab === 'environment-variables' && 'text-white'"
@click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'" @click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'"
href="#">Environment href="#">Environment
@@ -17,65 +16,28 @@
</div> </div>
<div class="w-full pl-8"> <div class="w-full pl-8">
<div x-cloak x-show="activeTab === 'service-stack'"> <div x-cloak x-show="activeTab === 'service-stack'">
<form wire:submit.prevent='submit' class="pb-4"> <form wire:submit.prevent='submit' class="flex flex-col gap-4 pb-2">
<div class="flex gap-2"> <div class="flex gap-2">
<h2 class="pb-4">Configuration </h2> <div>
<h2> Service Stack </h2>
<div>Configuration</div>
</div>
<x-forms.button type="submit">Save</x-forms.button> <x-forms.button type="submit">Save</x-forms.button>
<div x-cloak x-show="raw">
<x-forms.button class="w-64" @click.prevent="raw = !raw">Show Deployable
Compose</x-forms.button>
</div>
<div x-cloak x-show="raw === false">
<x-forms.button class="w-64" @click.prevent="raw = !raw">Show Source
Compose</x-forms.button>
</div>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input id="service.name" required label="Service Name" <x-forms.input id="service.name" required label="Service Name"
placeholder="My super wordpress site" /> placeholder="My super wordpress site" />
<x-forms.input id="service.description" label="Description" /> <x-forms.input id="service.description" label="Description" />
</div> </div>
</form>
<h2 class="pb-4"> Service Stack </h2>
<div class="grid grid-cols-1 gap-2">
@foreach ($service->applications as $application)
<a class="flex flex-col justify-center box"
href="{{ route('project.service.show', [...$parameters, 'service_name' => $application->name]) }}">
@if ($application->human_name)
{{ Str::headline($application->human_name) }}
@else
{{ Str::headline($application->name) }}
@endif
@if ($application->description)
<span class="text-xs">{{ $application->description }}</span>
@endif
@if ($application->fqdn)
<span class="text-xs">{{ $application->fqdn }}</span>
@endif
</a>
@endforeach
@foreach ($service->databases as $database)
<a class="flex flex-col justify-center box"
href="{{ route('project.service.show', [...$parameters, 'service_name' => $database->name]) }}">
@if ($database->human_name)
{{ Str::headline($database->human_name) }}
@else
{{ Str::headline($database->name) }}
@endif
@if ($database->description)
<span class="text-xs">{{ $database->description }}</span>
@endif
</a>
@endforeach
</div>
</div>
<div x-cloak x-show="activeTab === 'compose'">
<div x-cloak x-show="activeTab === 'compose'">
<div class="flex gap-2 pb-4">
<h2>Docker Compose</h2>
<div x-cloak x-show="raw">
<x-forms.button class="w-64" @click.prevent="raw = !raw">Show Deployable</x-forms.button>
<x-forms.button wire:click='save'>Save</x-forms.button>
</div>
<div x-cloak x-show="raw === false">
<x-forms.button class="w-64" @click.prevent="raw = !raw">Show Source</x-forms.button>
<x-forms.button disabled wire:click='save'>Save</x-forms.button>
</div>
</div>
<div x-cloak x-show="raw"> <div x-cloak x-show="raw">
<x-forms.textarea label="Docker Compose file" <x-forms.textarea label="Docker Compose file"
helper=" helper="
@@ -83,16 +45,74 @@
<br> <br>
- SERVICE_FQDN_*: FQDN - could be changable from the UI. (example: SERVICE_FQDN_GHOST)<br> - SERVICE_FQDN_*: FQDN - could be changable from the UI. (example: SERVICE_FQDN_GHOST)<br>
- SERVICE_URL_*: URL parsed from FQDN - could be changable from the UI. (example: SERVICE_URL_GHOST)<br> - SERVICE_URL_*: URL parsed from FQDN - could be changable from the UI. (example: SERVICE_URL_GHOST)<br>
- SERVICE_USER_*: Generated user, not encrypted in database (example: SERVICE_USER_MYSQL)<br> - SERVICE_BASE64_64_*: Generated 'base64' string with length of '64' (example: SERVICE_BASE64_64_GHOST, to generate 32 bit: SERVICE_BASE64_32_GHOST)<br>
- SERVICE_PASSWORD_*: Generated password, encrypted in database (example: SERVICE_PASSWORD_MYSQL)<br>" - SERVICE_USER_*: Generated user (example: SERVICE_USER_MYSQL)<br>
rows="20" id="service.docker_compose_raw"> - SERVICE_PASSWORD_*: Generated password (example: SERVICE_PASSWORD_MYSQL)<br>"
rows="6" id="service.docker_compose_raw">
</x-forms.textarea> </x-forms.textarea>
</div> </div>
<div x-cloak x-show="raw === false"> <div x-cloak x-show="raw === false">
<x-forms.textarea label="Actual Docker Compose file that will be deployed" readonly rows="20" id="service.docker_compose"> <x-forms.textarea label="Actual Docker Compose file that will be deployed" readonly
rows="6" id="service.docker_compose">
</x-forms.textarea> </x-forms.textarea>
</div> </div>
</form>
<div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-3">
@foreach ($service->applications as $application)
<a @class([
'border-l border-dashed border-red-500' => Str::of(
$application->status)->contains(['exited']),
'border-l border-dashed border-success' => Str::of(
$application->status)->contains(['running']),
'border-l border-dashed border-warning' => Str::of(
$application->status)->contains(['starting']),
'flex flex-col justify-center box',
])
href="{{ route('project.service.show', [...$parameters, 'service_name' => $application->name]) }}">
@if ($application->human_name)
{{ Str::headline($application->human_name) }}
@else
{{ Str::headline($application->name) }}
@endif
@if ($application->configuration_required)
<span class="text-xs text-error">(configuration required)</span>
@endif
@if ($application->description)
<span class="text-xs">{{ Str::limit($application->description, 60) }}</span>
@endif
@if ($application->fqdn)
<span class="text-xs">{{ Str::limit($application->fqdn, 60) }}</span>
@endif
<div class="text-xs">{{ $application->status }}</div>
</a>
@endforeach
@foreach ($databases as $database)
<a @class([
'border-l border-dashed border-red-500' => Str::of(
$database->status)->contains(['exited']),
'border-l border-dashed border-success' => Str::of(
$database->status)->contains(['running']),
'border-l border-dashed border-warning' => Str::of(
$database->status)->contains(['restarting']),
'flex flex-col justify-center box',
])
href="{{ route('project.service.show', [...$parameters, 'service_name' => $database->name]) }}">
@if ($database->human_name)
{{ Str::headline($database->human_name) }}
@else
{{ Str::headline($database->name) }}
@endif
@if ($database->configuration_required)
<span class="text-xs text-error">(configuration required)</span>
@endif
@if ($database->description)
<span class="text-xs">{{ Str::limit($database->description, 60) }}</span>
@endif
<div class="text-xs">{{ $database->status }}</div>
</a>
@endforeach
</div> </div>
</div> </div>
<div x-cloak x-show="activeTab === 'environment-variables'"> <div x-cloak x-show="activeTab === 'environment-variables'">
<div x-cloak x-show="activeTab === 'environment-variables'"> <div x-cloak x-show="activeTab === 'environment-variables'">

View File

@@ -19,14 +19,31 @@
</div> </div>
<div x-cloak x-show="activeTab === 'storages'"> <div x-cloak x-show="activeTab === 'storages'">
<livewire:project.shared.storages.all :resource="$serviceApplication" /> <livewire:project.shared.storages.all :resource="$serviceApplication" />
@if ($serviceApplication->fileStorages()->get()->count() > 0)
<h3 class="py-4">Mounted Files (binds)</h3>
<div class="flex flex-col gap-4">
@foreach ($serviceApplication->fileStorages()->get() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="{{ $loop->index }}" />
@endforeach
</div>
@endif
</div> </div>
@endisset @endisset
@isset($serviceDatabase) @isset($serviceDatabase)
<div x-cloak x-show="activeTab === 'general'" class="h-full"> <div x-cloak x-show="activeTab === 'general'" class="h-full">
<livewire:project.service.database :database="$serviceDatabase" /> <livewire:project.service.database :database="$serviceDatabase" />
</div> </div>
<div x-cloak x-show="activeTab === 'storages'"> <div x-cloak x-show="activeTab === 'storages'">
<livewire:project.shared.storages.all :resource="$serviceDatabase" /> <livewire:project.shared.storages.all :resource="$serviceDatabase" />
@if ($serviceDatabase->fileStorages()->get()->count() > 0)
<h3 class="py-4">Mounted Files (binds)</h3>
<div class="flex flex-col gap-4">
@foreach ($serviceDatabase->fileStorages()->get() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="{{ $loop->index }}" />
@endforeach
</div>
@endif
</div> </div>
@endisset @endisset
</div> </div>

View File

@@ -28,13 +28,13 @@
@endif @endif
@else @else
<form wire:submit.prevent='saveVariables(false)' class="flex flex-col gap-2"> <form wire:submit.prevent='saveVariables(false)' class="flex flex-col gap-2">
<x-forms.textarea rows=5 class="whitespace-pre-wrap" <x-forms.textarea rows=25 class="whitespace-pre-wrap"
id="variables"></x-forms.textarea> id="variables"></x-forms.textarea>
<x-forms.button type="submit" class="btn btn-primary">Save</x-forms.button> <x-forms.button type="submit" class="btn btn-primary">Save</x-forms.button>
</form> </form>
@if ($showPreview) @if ($showPreview)
<form wire:submit.prevent='saveVariables(true)' class="flex flex-col gap-2"> <form wire:submit.prevent='saveVariables(true)' class="flex flex-col gap-2">
<x-forms.textarea rows=5 class="whitespace-pre-wrap" label="Preview Environment Variables" <x-forms.textarea rows=25 class="whitespace-pre-wrap" label="Preview Environment Variables"
id="variablesPreview"></x-forms.textarea> id="variablesPreview"></x-forms.textarea>
<x-forms.button type="submit" class="btn btn-primary">Save</x-forms.button> <x-forms.button type="submit" class="btn btn-primary">Save</x-forms.button>
</form> </form>

Some files were not shown because too many files have changed in this diff Show More