diff --git a/README.md b/README.md index 56bee004e..27566cc67 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,21 @@ +![Latest Release Version](https://img.shields.io/badge/dynamic/json?labelColor=grey&color=6366f1&label=Latest_released_version&url=https%3A%2F%2Fcdn.coollabs.io%2Fcoolify%2Fversions.json&query=coolify.v4.version&style=for-the-badge +) + [![Bounty Issues](https://img.shields.io/static/v1?labelColor=grey&color=6366f1&label=Algora&message=%F0%9F%92%8E+Bounty+issues&style=for-the-badge)](https://console.algora.io/org/coollabsio/bounties/new) [![Open Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Fcoollabsio%2Fbounties%3Fstatus%3Dopen&style=for-the-badge)](https://console.algora.io/org/coollabsio/bounties?status=open) [![Rewarded Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Fcoollabsio%2Fbounties%3Fstatus%3Dcompleted&style=for-the-badge)](https://console.algora.io/org/coollabsio/bounties?status=completed) + # About the Project Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc. -It helps you to manage your servers, applications, databases on your own hardware, all you need is SSH connection. You can manage VPS, Bare Metal, Raspberry PI's anything. +It helps you manage your servers, applications, and databases on your own hardware; you only need an SSH connection. You can manage VPS, Bare Metal, Raspberry PIs, and anything else. -Imagine if you could have the ease of a cloud but with your own servers. That is **Coolify**. +Imagine having the ease of a cloud but with your own servers. That is **Coolify**. -No vendor lock-in, which means that all the configuration for your applications/databases/etc are saved to your server. So if you decide to stop using Coolify (oh nooo), you could still manage your running resources. You just lose the automations and all the magic. 🪄️ +No vendor lock-in, which means that all the configurations for your applications/databases/etc are saved to your server. So, if you decide to stop using Coolify (oh nooo), you could still manage your running resources. You lose the automations and all the magic. 🪄️ -For more information, take a look at our landing page [here](https://coolify.io). +For more information, take a look at our landing page at [coolify.io](https://coolify.io). # Installation @@ -22,36 +26,42 @@ You can find the installation script source [here](./scripts/install.sh). # Support -Contact us [here](https://coolify.io/docs/contact). +Contact us at [coolify.io/docs/contact](https://coolify.io/docs/contact). # Donations -To stay completely free, open-source, no feature behind paywall and evolve the project, we need your help. If you like Coolify, please consider donating to help us fund the future development of the project. +To stay completely free and open-source, with no feature behind the paywall and evolve the project, we need your help. If you like Coolify, please consider donating to help us fund the project's future development. -https://coolify.io/sponsorships +[coolify.io/sponsorships](https://coolify.io/sponsorships) Thank you so much! Special thanks to our biggest sponsors! cccareers logo -hetzner logo -logto logo -bc direct logo -quantcdn logo -arcjet logo - +hetzner logo +logto logo +bc direct logo +quantcdn logo +arcjet logo +supaguide logo +tigris logo +fractal logo +advin logo +trieve logo +blacksmith logo ## Github Sponsors ($40+) -SerpAPI -typebot - +SerpAPI +typebot + -Lightspeed.run - FlintCompany -American Cloud -CryptoJobsList -Thompson Edolo -UXWizz +Lightspeed.run + FlintCompany +American Cloud +CryptoJobsList +Codext +Thompson Edolo +UXWizz Younes Barrad Automaze Corentin Clichy @@ -83,9 +93,9 @@ Special thanks to our biggest sponsors! # Cloud -If you do not want to self-host Coolify, there is a paid cloud version available: https://app.coolify.io +If you do not want to self-host Coolify, there is a paid cloud version available: [app.coolify.io](https://app.coolify.io) -For more information & pricing, take a look at our landing page [here](https://coolify.io). +For more information & pricing, take a look at our landing page [coolify.io](https://coolify.io). ## Why should I use the Cloud version? The recommended way to use Coolify is to have one server for Coolify and one (or more) for the resources you are deploying. A server is around 4-5$/month. @@ -109,7 +119,7 @@ By subscribing to the cloud version, you get the Coolify server for the same pri

-Coolify - An open-source & self-hostable Heroku, Netlify alternative | Product Hunt +Coolify - An open-source & self-hostable Heroku, Netlify alternative | Product Hunt coollabsio%2Fcoolify | Trendshift diff --git a/app/Actions/Database/StopDatabaseProxy.php b/app/Actions/Database/StopDatabaseProxy.php index 984225435..1b262c898 100644 --- a/app/Actions/Database/StopDatabaseProxy.php +++ b/app/Actions/Database/StopDatabaseProxy.php @@ -2,6 +2,7 @@ namespace App\Actions\Database; +use App\Events\DatabaseStatusChanged; use App\Models\ServiceDatabase; use App\Models\StandaloneClickhouse; use App\Models\StandaloneDragonfly; @@ -28,5 +29,6 @@ class StopDatabaseProxy instant_remote_process(["docker rm -f {$uuid}-proxy"], $server); $database->is_public = false; $database->save(); + DatabaseStatusChanged::dispatch(); } } diff --git a/app/Actions/Server/StartSentinel.php b/app/Actions/Server/StartSentinel.php index eea429c79..b79bc8f67 100644 --- a/app/Actions/Server/StartSentinel.php +++ b/app/Actions/Server/StartSentinel.php @@ -12,12 +12,15 @@ class StartSentinel public function handle(Server $server, $version = 'latest', bool $restart = false) { if ($restart) { - instant_remote_process(['docker rm -f coolify-sentinel'], $server, false); + StopSentinel::run($server); } + $metrics_history = $server->settings->metrics_history_days; + $refresh_rate = $server->settings->metrics_refresh_rate_seconds; + $token = $server->settings->metrics_token; instant_remote_process([ - "docker run --rm --pull always -d -e \"SCHEDULER=true\" --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify/metrics:/app/metrics -v /data/coolify/logs:/app/logs --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 ghcr.io/coollabsio/sentinel:$version", + "docker run --rm --pull always -d -e \"TOKEN={$token}\" -e \"SCHEDULER=true\" -e \"METRICS_HISTORY={$metrics_history}\" -e \"REFRESH_RATE={$refresh_rate}\" --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify/metrics:/app/metrics -v /data/coolify/logs:/app/logs --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 ghcr.io/coollabsio/sentinel:$version", 'chown -R 9999:root /data/coolify/metrics /data/coolify/logs', 'chmod -R 700 /data/coolify/metrics /data/coolify/logs', - ], $server, false); + ], $server, true); } } diff --git a/app/Actions/Server/StopSentinel.php b/app/Actions/Server/StopSentinel.php new file mode 100644 index 000000000..21ffca3bd --- /dev/null +++ b/app/Actions/Server/StopSentinel.php @@ -0,0 +1,16 @@ +option('full-cleanup'); $cleanup_deployments = $this->option('cleanup-deployments'); + + $this->replace_slash_in_environment_name(); if ($cleanup_deployments) { echo "Running cleanup deployments.\n"; $this->cleanup_in_progress_application_deployments(); @@ -150,4 +153,15 @@ class Init extends Command echo "Error: {$e->getMessage()}\n"; } } + + private function replace_slash_in_environment_name() + { + $environments = Environment::all(); + foreach ($environments as $environment) { + if (str_contains($environment->name, '/')) { + $environment->name = str_replace('/', '-', $environment->name); + $environment->save(); + } + } + } } diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index e2698d90e..f529f63b9 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -61,7 +61,7 @@ class Kernel extends ConsoleKernel { $servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4'); foreach ($servers as $server) { - if (config('coolify.is_sentinel_enabled')) { + if ($server->isSentinelEnabled()) { $schedule->job(new PullSentinelImageJob($server))->everyFiveMinutes()->onOneServer(); } $schedule->job(new PullHelperImageJob($server))->everyFiveMinutes()->onOneServer(); diff --git a/app/Http/Controllers/Api/Applications.php b/app/Http/Controllers/Api/Applications.php new file mode 100644 index 000000000..a638adecd --- /dev/null +++ b/app/Http/Controllers/Api/Applications.php @@ -0,0 +1,183 @@ +get(); + $applications = collect(); + $applications->push($projects->pluck('applications')->flatten()); + $applications = $applications->flatten(); + + return response()->json($applications); + } + + public function application_by_uuid(Request $request) + { + $teamId = get_team_id_from_token(); + if (is_null($teamId)) { + return invalid_token(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['error' => 'UUID is required.'], 400); + } + $application = Application::where('uuid', $uuid)->first(); + if (! $application) { + return response()->json(['error' => 'Application not found.'], 404); + } + + return response()->json($application); + } + + public function update_by_uuid(Request $request) + { + $teamId = get_team_id_from_token(); + if (is_null($teamId)) { + return invalid_token(); + } + + if ($request->collect()->count() == 0) { + return response()->json([ + 'message' => 'No data provided.', + ], 400); + } + $application = Application::where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'success' => false, + 'message' => 'Application not found', + ], 404); + } + ray($request->collect()); + + // if ($request->has('domains')) { + // $existingDomains = explode(',', $application->fqdn); + // $newDomains = $request->domains; + // $filteredNewDomains = array_filter($newDomains, function ($domain) use ($existingDomains) { + // return ! in_array($domain, $existingDomains); + // }); + // $mergedDomains = array_unique(array_merge($existingDomains, $filteredNewDomains)); + // $application->fqdn = implode(',', $mergedDomains); + // $application->custom_labels = base64_encode(implode("\n ", generateLabelsApplication($application))); + // $application->save(); + // } + + return response()->json([ + 'message' => 'Application updated successfully.', + 'application' => serialize_api_response($application), + ]); + } + + public function action_deploy(Request $request) + { + $teamId = get_team_id_from_token(); + if (is_null($teamId)) { + return invalid_token(); + } + $force = $request->query->get('force') ?? false; + $instant_deploy = $request->query->get('instant_deploy') ?? false; + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['error' => 'UUID is required.'], 400); + } + $application = Application::where('uuid', $uuid)->first(); + if (! $application) { + return response()->json(['error' => 'Application not found.'], 404); + } + + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + force_rebuild: $force, + is_api: true, + no_questions_asked: $instant_deploy + ); + + return response()->json( + [ + 'message' => 'Deployment request queued.', + 'deployment_uuid' => $deployment_uuid->toString(), + 'deployment_api_url' => base_url().'/api/v1/deployment/'.$deployment_uuid->toString(), + ], + 200 + ); + } + + public function action_stop(Request $request) + { + $teamId = get_team_id_from_token(); + if (is_null($teamId)) { + return invalid_token(); + } + $uuid = $request->route('uuid'); + $sync = $request->query->get('sync') ?? false; + if (! $uuid) { + return response()->json(['error' => 'UUID is required.'], 400); + } + $application = Application::where('uuid', $uuid)->first(); + if (! $application) { + return response()->json(['error' => 'Application not found.'], 404); + } + if ($sync) { + StopApplication::run($application); + + return response()->json(['message' => 'Stopped the application.'], 200); + } else { + StopApplication::dispatch($application); + + return response()->json(['message' => 'Stopping request queued.'], 200); + } + } + + public function action_restart(Request $request) + { + $teamId = get_team_id_from_token(); + if (is_null($teamId)) { + return invalid_token(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['error' => 'UUID is required.'], 400); + } + $application = Application::where('uuid', $uuid)->first(); + if (! $application) { + return response()->json(['error' => 'Application not found.'], 404); + } + + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + restart_only: true, + is_api: true, + ); + + return response()->json( + [ + 'message' => 'Restart request queued.', + 'deployment_uuid' => $deployment_uuid->toString(), + 'deployment_api_url' => base_url().'/api/v1/deployment/'.$deployment_uuid->toString(), + ], + 200 + ); + + } +} diff --git a/app/Http/Controllers/Api/Deploy.php b/app/Http/Controllers/Api/Deploy.php index d2abe2e31..f7a34facc 100644 --- a/app/Http/Controllers/Api/Deploy.php +++ b/app/Http/Controllers/Api/Deploy.php @@ -38,7 +38,25 @@ class Deploy extends Controller 'status', ])->sortBy('id')->toArray(); - return response()->json($deployments_per_server, 200); + return response()->json(serialize_api_response($deployments_per_server), 200); + } + + public function deployment_by_uuid(Request $request) + { + $teamId = get_team_id_from_token(); + if (is_null($teamId)) { + return invalid_token(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['error' => 'UUID is required.'], 400); + } + $deployment = ApplicationDeploymentQueue::where('deployment_uuid', $uuid)->first()->makeHidden('logs'); + if (! $deployment) { + return response()->json(['error' => 'Deployment not found.'], 404); + } + + return response()->json(serialize_api_response($deployment), 200); } public function deploy(Request $request) diff --git a/app/Http/Controllers/Api/Domains.php b/app/Http/Controllers/Api/Domains.php index c27ddf620..6473b64de 100644 --- a/app/Http/Controllers/Api/Domains.php +++ b/app/Http/Controllers/Api/Domains.php @@ -3,102 +3,52 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; -use App\Models\InstanceSettings; -use App\Models\Project as ModelsProject; +use App\Models\Application; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Validator; class Domains extends Controller { - public function domains(Request $request) + public function deleteDomains(Request $request) { $teamId = get_team_id_from_token(); if (is_null($teamId)) { return invalid_token(); } - $projects = ModelsProject::where('team_id', $teamId)->get(); - $domains = collect(); - $applications = $projects->pluck('applications')->flatten(); - $settings = InstanceSettings::get(); - if ($applications->count() > 0) { - foreach ($applications as $application) { - $ip = $application->destination->server->ip; - $fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) { - return str($fqdn)->replace('http://', '')->replace('https://', '')->replace('/', ''); - }); - if ($ip === 'host.docker.internal') { - if ($settings->public_ipv4) { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $settings->public_ipv4, - ]); - } - if ($settings->public_ipv6) { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $settings->public_ipv6, - ]); - } - if (! $settings->public_ipv4 && ! $settings->public_ipv6) { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $ip, - ]); - } - } else { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $ip, - ]); - } - } - } - $services = $projects->pluck('services')->flatten(); - if ($services->count() > 0) { - foreach ($services as $service) { - $service_applications = $service->applications; - if ($service_applications->count() > 0) { - foreach ($service_applications as $application) { - $fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) { - return str($fqdn)->replace('http://', '')->replace('https://', '')->replace('/', ''); - }); - if ($ip === 'host.docker.internal') { - if ($settings->public_ipv4) { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $settings->public_ipv4, - ]); - } - if ($settings->public_ipv6) { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $settings->public_ipv6, - ]); - } - if (! $settings->public_ipv4 && ! $settings->public_ipv6) { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $ip, - ]); - } - } else { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $ip, - ]); - } - } - } - } - } - $domains = $domains->groupBy('ip')->map(function ($domain) { - return $domain->pluck('domain')->flatten(); - })->map(function ($domain, $ip) { - return [ - 'ip' => $ip, - 'domains' => $domain, - ]; - })->values(); + $validator = Validator::make($request->all(), [ + 'uuid' => 'required|string|exists:applications,uuid', + 'domains' => 'required|array', + 'domains.*' => 'required|string|distinct', + ]); - return response()->json($domains); + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation failed', + 'errors' => $validator->errors(), + ], 422); + } + + $application = Application::where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'success' => false, + 'message' => 'Application not found', + ], 404); + } + + $existingDomains = explode(',', $application->fqdn); + $domainsToDelete = $request->domains; + $updatedDomains = array_diff($existingDomains, $domainsToDelete); + $application->fqdn = implode(',', $updatedDomains); + $application->custom_labels = base64_encode(implode("\n ", generateLabelsApplication($application))); + $application->save(); + + return response()->json([ + 'success' => true, + 'message' => 'Domains updated successfully', + 'application' => $application, + ]); } } diff --git a/app/Http/Controllers/Api/Server.php b/app/Http/Controllers/Api/Server.php index 9f88a3b28..7a6090bf8 100644 --- a/app/Http/Controllers/Api/Server.php +++ b/app/Http/Controllers/Api/Server.php @@ -3,6 +3,9 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; +use App\Models\Application; +use App\Models\InstanceSettings; +use App\Models\Project; use App\Models\Server as ModelsServer; use Illuminate\Http\Request; @@ -59,4 +62,106 @@ class Server extends Controller return response()->json($server); } + + public function get_domains_by_server(Request $request) + { + $teamId = get_team_id_from_token(); + if (is_null($teamId)) { + return invalid_token(); + } + $uuid = $request->query->get('uuid'); + if ($uuid) { + $domains = Application::getDomainsByUuid($uuid); + + return response()->json([ + 'uuid' => $uuid, + 'domains' => $domains, + ]); + } + $projects = Project::where('team_id', $teamId)->get(); + $domains = collect(); + $applications = $projects->pluck('applications')->flatten(); + $settings = InstanceSettings::get(); + if ($applications->count() > 0) { + foreach ($applications as $application) { + $ip = $application->destination->server->ip; + $fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) { + return str($fqdn)->replace('http://', '')->replace('https://', '')->replace('/', ''); + }); + if ($ip === 'host.docker.internal') { + if ($settings->public_ipv4) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $settings->public_ipv4, + ]); + } + if ($settings->public_ipv6) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $settings->public_ipv6, + ]); + } + if (! $settings->public_ipv4 && ! $settings->public_ipv6) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $ip, + ]); + } + } else { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $ip, + ]); + } + } + } + $services = $projects->pluck('services')->flatten(); + if ($services->count() > 0) { + foreach ($services as $service) { + $service_applications = $service->applications; + if ($service_applications->count() > 0) { + foreach ($service_applications as $application) { + $fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) { + return str($fqdn)->replace('http://', '')->replace('https://', '')->replace('/', ''); + }); + if ($ip === 'host.docker.internal') { + if ($settings->public_ipv4) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $settings->public_ipv4, + ]); + } + if ($settings->public_ipv6) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $settings->public_ipv6, + ]); + } + if (! $settings->public_ipv4 && ! $settings->public_ipv6) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $ip, + ]); + } + } else { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $ip, + ]); + } + } + } + } + } + $domains = $domains->groupBy('ip')->map(function ($domain) { + return $domain->pluck('domain')->flatten(); + })->map(function ($domain, $ip) { + return [ + 'ip' => $ip, + 'domains' => $domain, + ]; + })->values(); + + return response()->json($domains); + } } diff --git a/app/Http/Controllers/OauthController.php b/app/Http/Controllers/OauthController.php index 5b17fe926..9569e8cfa 100644 --- a/app/Http/Controllers/OauthController.php +++ b/app/Http/Controllers/OauthController.php @@ -2,8 +2,10 @@ namespace App\Http\Controllers; +use App\Models\InstanceSettings; use App\Models\User; use Illuminate\Support\Facades\Auth; +use Symfony\Component\HttpKernel\Exception\HttpException; class OauthController extends Controller { @@ -20,6 +22,11 @@ class OauthController extends Controller $oauthUser = get_socialite_provider($provider)->user(); $user = User::whereEmail($oauthUser->email)->first(); if (! $user) { + $settings = InstanceSettings::get(); + if (! $settings->is_registration_enabled) { + abort(403, 'Registration is disabled'); + } + $user = User::create([ 'name' => $oauthUser->name, 'email' => $oauthUser->email, @@ -31,7 +38,9 @@ class OauthController extends Controller } catch (\Exception $e) { ray($e->getMessage()); - return redirect()->route('login')->withErrors([__('auth.failed.callback')]); + $errorCode = $e instanceof HttpException ? 'auth.failed' : 'auth.failed.callback'; + + return redirect()->route('login')->withErrors([__($errorCode)]); } } } diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php index b9035b755..059438ff4 100644 --- a/app/Http/Controllers/Webhook/Bitbucket.php +++ b/app/Http/Controllers/Webhook/Bitbucket.php @@ -130,12 +130,23 @@ class Bitbucket extends Controller $deployment_uuid = new Cuid2(7); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { - ApplicationPreview::create([ - 'git_type' => 'bitbucket', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, - ]); + if ($application->build_pack === 'dockercompose') { + $pr_app = ApplicationPreview::create([ + 'git_type' => 'bitbucket', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + 'docker_compose_domains' => $application->docker_compose_domains, + ]); + $pr_app->generate_preview_fqdn_compose(); + } else { + ApplicationPreview::create([ + 'git_type' => 'bitbucket', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } } queue_application_deployment( application: $application, diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php index 388481949..e6d91efd6 100644 --- a/app/Http/Controllers/Webhook/Gitea.php +++ b/app/Http/Controllers/Webhook/Gitea.php @@ -165,12 +165,24 @@ class Gitea extends Controller $deployment_uuid = new Cuid2(7); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { - ApplicationPreview::create([ - 'git_type' => 'gitea', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, - ]); + if ($application->build_pack === 'dockercompose') { + $pr_app = ApplicationPreview::create([ + 'git_type' => 'gitea', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + 'docker_compose_domains' => $application->docker_compose_domains, + ]); + $pr_app->generate_preview_fqdn_compose(); + } else { + ApplicationPreview::create([ + 'git_type' => 'gitea', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } + } queue_application_deployment( application: $application, diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index 403438193..a030e31ca 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -170,12 +170,23 @@ class Github extends Controller $deployment_uuid = new Cuid2(7); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { - ApplicationPreview::create([ - 'git_type' => 'github', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, - ]); + if ($application->build_pack === 'dockercompose') { + $pr_app = ApplicationPreview::create([ + 'git_type' => 'github', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + 'docker_compose_domains' => $application->docker_compose_domains, + ]); + $pr_app->generate_preview_fqdn_compose(); + } else { + ApplicationPreview::create([ + 'git_type' => 'github', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } } queue_application_deployment( application: $application, diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php index a3d7712eb..f6e6cf7e7 100644 --- a/app/Http/Controllers/Webhook/Gitlab.php +++ b/app/Http/Controllers/Webhook/Gitlab.php @@ -180,12 +180,23 @@ class Gitlab extends Controller $deployment_uuid = new Cuid2(7); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { - ApplicationPreview::create([ - 'git_type' => 'gitlab', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, - ]); + if ($application->build_pack === 'dockercompose') { + $pr_app = ApplicationPreview::create([ + 'git_type' => 'gitlab', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + 'docker_compose_domains' => $application->docker_compose_domains, + ]); + $pr_app->generate_preview_fqdn_compose(); + } else { + ApplicationPreview::create([ + 'git_type' => 'gitlab', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } } queue_application_deployment( application: $application, diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 72d8c0ad1..2944b746d 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -9,6 +9,7 @@ use App\Events\ApplicationStatusChanged; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationPreview; +use App\Models\EnvironmentVariable; use App\Models\GithubApp; use App\Models\GitlabApp; use App\Models\Server; @@ -339,7 +340,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private function post_deployment() { if ($this->server->isProxyShouldRun()) { - GetContainersStatus::dispatch($this->server); + GetContainersStatus::dispatch($this->server)->onQueue('high'); // dispatch(new ContainerStatusJob($this->server)); } $this->next(ApplicationDeploymentStatus::FINISHED->value); @@ -827,6 +828,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) { $envs->push("COOLIFY_BRANCH={$local_branch}"); } + if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { + $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}"); + } foreach ($sorted_environment_variables_preview as $env) { $real_value = $env->real_value; if ($env->version === '4.0.0-beta.239') { @@ -868,6 +872,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { $envs->push("COOLIFY_BRANCH={$local_branch}"); } + if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { + $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}"); + } foreach ($sorted_environment_variables as $env) { $real_value = $env->real_value; if ($env->version === '4.0.0-beta.239') { @@ -877,7 +884,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $real_value = '\''.$real_value.'\''; } else { $real_value = escapeEnvVariables($env->real_value); - ray($real_value); } } $envs->push($env->key.'='.$real_value); @@ -946,9 +952,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } } - private function framework_based_notification() + private function laravel_finetunes() { - // Laravel old env variables if ($this->pull_request_id === 0) { $nixpacks_php_fallback_path = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first(); $nixpacks_php_root_dir = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first(); @@ -956,9 +961,22 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $nixpacks_php_fallback_path = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first(); $nixpacks_php_root_dir = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first(); } - if ($nixpacks_php_fallback_path?->value === '/index.php' && $nixpacks_php_root_dir?->value === '/app/public' && $this->newVersionIsHealthy === false) { - $this->application_deployment_queue->addLogEntry('There was a change in how Laravel is deployed. Please update your environment variables to match the new deployment method. More details here: https://coolify.io/docs/resources/laravel', 'stderr'); + if (! $nixpacks_php_fallback_path) { + $nixpacks_php_fallback_path = new EnvironmentVariable(); + $nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH'; + $nixpacks_php_fallback_path->value = '/index.php'; + $nixpacks_php_fallback_path->application_id = $this->application->id; + $nixpacks_php_fallback_path->save(); } + if (! $nixpacks_php_root_dir) { + $nixpacks_php_root_dir = new EnvironmentVariable(); + $nixpacks_php_root_dir->key = 'NIXPACKS_PHP_ROOT_DIR'; + $nixpacks_php_root_dir->value = '/app/public'; + $nixpacks_php_root_dir->application_id = $this->application->id; + $nixpacks_php_root_dir->save(); + } + + return [$nixpacks_php_fallback_path, $nixpacks_php_root_dir]; } private function rolling_update() @@ -1005,7 +1023,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->application_deployment_queue->addLogEntry('Rolling update completed.'); } } - $this->framework_based_notification(); } private function health_check() @@ -1366,17 +1383,20 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue throw new RuntimeException('Nixpacks failed to detect the application type. Please check the documentation of Nixpacks: https://nixpacks.com/docs/providers'); } } + if ($this->saved_outputs->get('nixpacks_plan')) { $this->nixpacks_plan = $this->saved_outputs->get('nixpacks_plan'); if ($this->nixpacks_plan) { $this->application_deployment_queue->addLogEntry("Found application type: {$this->nixpacks_type}."); $this->application_deployment_queue->addLogEntry("If you need further customization, please check the documentation of Nixpacks: https://nixpacks.com/docs/providers/{$this->nixpacks_type}"); $parsed = Toml::Parse($this->nixpacks_plan); + // Do any modifications here $this->generate_env_variables(); $merged_envs = $this->env_args->merge(collect(data_get($parsed, 'variables', []))); $aptPkgs = data_get($parsed, 'phases.setup.aptPkgs', []); if (count($aptPkgs) === 0) { + $aptPkgs = ['curl', 'wget']; data_set($parsed, 'phases.setup.aptPkgs', ['curl', 'wget']); } else { if (! in_array('curl', $aptPkgs)) { @@ -1388,6 +1408,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue data_set($parsed, 'phases.setup.aptPkgs', $aptPkgs); } data_set($parsed, 'variables', $merged_envs->toArray()); + $is_laravel = data_get($parsed, 'variables.IS_LARAVEL', false); + if ($is_laravel) { + $variables = $this->laravel_finetunes(); + data_set($parsed, 'variables.NIXPACKS_PHP_FALLBACK_PATH', $variables[0]->value); + data_set($parsed, 'variables.NIXPACKS_PHP_ROOT_DIR', $variables[1]->value); + } $this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT); $this->application_deployment_queue->addLogEntry("Final Nixpacks plan: {$this->nixpacks_plan}", hidden: true); } @@ -1841,13 +1867,25 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]); if ($this->force_rebuild) { $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir}"), 'hidden' => true, + executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true, ]); + $build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}"; } else { $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir}"), 'hidden' => true, + executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true, ]); + $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}"; } + + $base64_build_command = base64_encode($build_command); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true, + ] + ); $this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]); } else { if ($this->force_rebuild) { @@ -1866,7 +1904,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); ] ); } - $dockerfile = base64_encode("FROM {$this->application->static_image} WORKDIR /usr/share/nginx/html/ LABEL coolify.deploymentId={$this->deployment_uuid} @@ -1929,13 +1966,24 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]); if ($this->force_rebuild) { $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir}"), 'hidden' => true, + executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true, ]); + $build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; } else { $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir}"), 'hidden' => true, + executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true, ]); + $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; } + $base64_build_command = base64_encode($build_command); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true, + ] + ); $this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]); } else { if ($this->force_rebuild) { @@ -2184,10 +2232,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); ray($code); if ($code !== 69420) { // 69420 means failed to push the image to the registry, so we don't need to remove the new version as it is the currently running one - $this->application_deployment_queue->addLogEntry('Deployment failed. Removing the new version of your application.', 'stderr'); - $this->execute_remote_command( - ["docker rm -f $this->container_name >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true] - ); + if ($this->application->settings->is_consistent_container_name_enabled || isset($this->application->settings->custom_internal_name)) { + // do not remove already running container + } else { + $this->application_deployment_queue->addLogEntry('Deployment failed. Removing the new version of your application.', 'stderr'); + $this->execute_remote_command( + ["docker rm -f $this->container_name >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true] + ); + } } } } diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php index 50728ba86..e637fb6d4 100644 --- a/app/Jobs/DockerCleanupJob.php +++ b/app/Jobs/DockerCleanupJob.php @@ -60,7 +60,7 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue Log::info('No need to clean up '.$this->server->name); } } catch (\Throwable $e) { - send_internal_notification('DockerCleanupJob failed with: '.$e->getMessage()); + // send_internal_notification('DockerCleanupJob failed with: '.$e->getMessage()); ray($e->getMessage()); throw $e; } diff --git a/app/Jobs/PullSentinelImageJob.php b/app/Jobs/PullSentinelImageJob.php index 30b36d99f..f8c769382 100644 --- a/app/Jobs/PullSentinelImageJob.php +++ b/app/Jobs/PullSentinelImageJob.php @@ -50,7 +50,7 @@ class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue } ray('Sentinel image is up to date'); } catch (\Throwable $e) { - send_internal_notification('PullSentinelImageJob failed with: '.$e->getMessage()); + // send_internal_notification('PullSentinelImageJob failed with: '.$e->getMessage()); ray($e->getMessage()); throw $e; } diff --git a/app/Jobs/ServerStatusJob.php b/app/Jobs/ServerStatusJob.php index c30dadff3..c7321a74c 100644 --- a/app/Jobs/ServerStatusJob.php +++ b/app/Jobs/ServerStatusJob.php @@ -46,7 +46,7 @@ class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue if ($this->server->isFunctional()) { $this->cleanup(notify: false); $this->remove_unnecessary_coolify_yaml(); - if (config('coolify.is_sentinel_enabled')) { + if ($this->server->isSentinelEnabled()) { $this->server->checkSentinel(); } } diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index b787ed0cc..7acf5ed87 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -12,7 +12,7 @@ use Livewire\Component; class Index extends Component { - protected $listeners = ['serverInstalled' => 'validateServer']; + protected $listeners = ['refreshBoardingIndex' => 'validateServer']; public string $currentState = 'welcome'; diff --git a/app/Livewire/MonacoEditor.php b/app/Livewire/MonacoEditor.php new file mode 100644 index 000000000..156c63d3a --- /dev/null +++ b/app/Livewire/MonacoEditor.php @@ -0,0 +1,51 @@ + '$refresh', + ]; + + public function __construct( + public ?string $id, + public ?string $name, + public ?string $type, + public ?string $monacoContent, + public ?string $value, + public ?string $label, + public ?string $placeholder, + public bool $required, + public bool $disabled, + public bool $readonly, + public bool $allowTab, + public bool $spellcheck, + public ?string $helper, + public bool $realtimeValidation, + public bool $allowToPeak, + public string $defaultClass, + public string $defaultClassInput, + public ?string $language + + ) { + // + } + + public function render() + { + if (is_null($this->id)) { + $this->id = new Cuid2(7); + } + + if (is_null($this->name)) { + $this->name = $this->id; + } + + return view('components.forms.monaco-editor'); + } +} diff --git a/app/Livewire/Project/Application/DeploymentNavbar.php b/app/Livewire/Project/Application/DeploymentNavbar.php index cbbe98d99..b3e39d23d 100644 --- a/app/Livewire/Project/Application/DeploymentNavbar.php +++ b/app/Livewire/Project/Application/DeploymentNavbar.php @@ -54,9 +54,9 @@ class DeploymentNavbar extends Component public function cancel() { + $kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}"; + $server_id = $this->application_deployment_queue->server_id ?? $this->application->destination->server_id; try { - $kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}"; - $server_id = $this->application_deployment_queue->server_id ?? $this->application->destination->server_id; $server = Server::find($server_id); if ($this->application_deployment_queue->logs) { $previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR); @@ -84,6 +84,7 @@ class DeploymentNavbar extends Component 'current_process_id' => null, 'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value, ]); + next_after_cancel($server); } } } diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 60cdee48e..06ff7b1de 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -347,7 +347,9 @@ class General extends Component public function submit($showToaster = true) { try { - $this->set_redirect(); + if ($this->application->isDirty('redirect')) { + $this->set_redirect(); + } $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php index d224f4a9d..feb54c7f0 100644 --- a/app/Livewire/Project/Application/Heading.php +++ b/app/Livewire/Project/Application/Heading.php @@ -45,7 +45,7 @@ class Heading extends Component public function check_status($showNotification = false) { if ($this->application->destination->server->isFunctional()) { - GetContainersStatus::dispatch($this->application->destination->server); + GetContainersStatus::dispatch($this->application->destination->server)->onQueue('high'); // dispatch(new ContainerStatusJob($this->application->destination->server)); } else { dispatch(new ServerStatusJob($this->application->destination->server)); diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index ca911339e..df64c3fd3 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -131,6 +131,12 @@ class Previews extends Component } } + public function add_and_deploy(int $pull_request_id, ?string $pull_request_html_url = null) + { + $this->add($pull_request_id, $pull_request_html_url); + $this->deploy($pull_request_id, $pull_request_html_url); + } + public function deploy(int $pull_request_id, ?string $pull_request_html_url = null) { try { @@ -180,7 +186,7 @@ class Previews extends Component instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false); } } - GetContainersStatus::dispatchSync($this->application->destination->server); + GetContainersStatus::dispatchSync($this->application->destination->server)->onQueue('high'); $this->dispatch('reloadWindow'); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php index 61dafa76f..ae88ac12b 100644 --- a/app/Livewire/Project/Database/Heading.php +++ b/app/Livewire/Project/Database/Heading.php @@ -12,7 +12,6 @@ use App\Actions\Database\StartPostgresql; use App\Actions\Database\StartRedis; use App\Actions\Database\StopDatabase; use App\Actions\Docker\GetContainersStatus; -use App\Jobs\ContainerStatusJob; use Livewire\Component; class Heading extends Component diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php index 38cac2e5c..1c5d39055 100644 --- a/app/Livewire/Project/Database/Postgresql/General.php +++ b/app/Livewire/Project/Database/Postgresql/General.php @@ -25,7 +25,17 @@ class General extends Component public ?string $db_url_public = null; - protected $listeners = ['refresh', 'save_init_script', 'delete_init_script']; + public function getListeners() + { + $userId = auth()->user()->id; + + return [ + "echo-private:user.{$userId},DatabaseStatusChanged" => 'database_stopped', + 'refresh', + 'save_init_script', + 'delete_init_script', + ]; + } protected $rules = [ 'database.name' => 'required', @@ -69,6 +79,11 @@ class General extends Component $this->server = data_get($this->database, 'destination.server'); } + public function database_stopped() + { + $this->dispatch('success', 'Database proxy stopped. Database is no longer publicly accessible.'); + } + public function instantSaveAdvanced() { try { diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index 739061f1f..7ac7883dc 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -128,8 +128,8 @@ class PublicGitRepository extends Component ) { $this->repository_url = $this->repository_url.'.git'; } - if (str($this->repository_url)->contains('github.com')) { - $this->repository_url = str($this->repository_url)->before('.git')->value(); + if (str($this->repository_url)->contains('github.com') && str($this->repository_url)->endsWith('.git')) { + $this->repository_url = str($this->repository_url)->beforeLast('.git')->value(); } } catch (\Throwable $e) { return handleError($e, $this); @@ -140,7 +140,6 @@ class PublicGitRepository extends Component $this->get_branch(); $this->selected_branch = $this->git_branch; } catch (\Throwable $e) { - ray($e->getMessage()); if (! $this->branch_found && $this->git_branch == 'main') { try { $this->git_branch = 'master'; diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php index b8d186dab..b25290f71 100644 --- a/app/Livewire/Project/New/Select.php +++ b/app/Livewire/Project/New/Select.php @@ -176,10 +176,12 @@ class Select extends Component return; } - // if (count($this->servers) === 1) { - // $server = $this->servers->first(); - // $this->setServer($server); - // } + if (count($this->servers) === 1) { + $server = $this->servers->first(); + if ($server instanceof Server) { + $this->setServer($server); + } + } if (! is_null($this->server)) { $foundServer = $this->servers->where('id', $this->server->id)->first(); if ($foundServer) { @@ -195,6 +197,13 @@ class Select extends Component $this->server = $server; $this->standaloneDockers = $server->standaloneDockers; $this->swarmDockers = $server->swarmDockers; + $count = count($this->standaloneDockers) + count($this->swarmDockers); + if ($count === 1) { + $docker = $this->standaloneDockers->first() ?? $this->swarmDockers->first(); + if ($docker) { + $this->setDestination($docker->uuid); + } + } $this->current_step = 'destinations'; } diff --git a/app/Livewire/Project/Resource/EnvironmentSelect.php b/app/Livewire/Project/Resource/EnvironmentSelect.php new file mode 100644 index 000000000..efb1b6ca2 --- /dev/null +++ b/app/Livewire/Project/Resource/EnvironmentSelect.php @@ -0,0 +1,35 @@ +selectedEnvironment = request()->route('environment_name'); + $this->project_uuid = request()->route('project_uuid'); + } + + public function updatedSelectedEnvironment($value) + { + if ($value === 'edit') { + return redirect()->route('project.show', [ + 'project_uuid' => $this->project_uuid, + ]); + } else { + return redirect()->route('project.resource.index', [ + 'project_uuid' => $this->project_uuid, + 'environment_name' => $value, + ]); + } + } +} diff --git a/app/Livewire/Project/Service/EditCompose.php b/app/Livewire/Project/Service/EditCompose.php index fd4d684b1..c9bdf12fc 100644 --- a/app/Livewire/Project/Service/EditCompose.php +++ b/app/Livewire/Project/Service/EditCompose.php @@ -11,6 +11,8 @@ class EditCompose extends Component public $serviceId; + protected $listeners = ['refreshEnvs' => 'mount']; + protected $rules = [ 'service.docker_compose_raw' => 'required', 'service.docker_compose' => 'required', diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php index 05917f895..a1b613a43 100644 --- a/app/Livewire/Project/Service/StackForm.php +++ b/app/Livewire/Project/Service/StackForm.php @@ -75,7 +75,6 @@ class StackForm extends Component $this->service->parse(); $this->service->refresh(); $this->service->saveComposeConfigs(); - $this->dispatch('refreshStacks'); $this->dispatch('refreshEnvs'); $this->dispatch('success', 'Service saved.'); } catch (\Throwable $e) { diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index 4c06bfe23..d67dae19e 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -112,7 +112,6 @@ class All extends Component $this->resource->environment_variables_preview()->whereNotIn('key', array_keys($variables))->delete(); } else { $variables = parseEnvFormatToArray($this->variables); - ray($variables, $this->variables); $this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->delete(); } foreach ($variables as $key => $variable) { diff --git a/app/Livewire/Project/Shared/Logs.php b/app/Livewire/Project/Shared/Logs.php index 008d743ed..5af0a6a50 100644 --- a/app/Livewire/Project/Shared/Logs.php +++ b/app/Livewire/Project/Shared/Logs.php @@ -59,15 +59,6 @@ class Logs extends Component } } - public function loadMetrics() - { - return; - $server = data_get($this->resource, 'destination.server'); - if ($server->isFunctional()) { - $this->cpu = $server->getMetrics(); - } - } - public function mount() { try { @@ -122,7 +113,6 @@ class Logs extends Component } - $this->loadMetrics(); } catch (\Exception $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Shared/Metrics.php b/app/Livewire/Project/Shared/Metrics.php new file mode 100644 index 000000000..d9d7dd3ef --- /dev/null +++ b/app/Livewire/Project/Shared/Metrics.php @@ -0,0 +1,64 @@ +poll || $this->interval <= 10) { + $this->loadData(); + if ($this->interval > 10) { + $this->poll = false; + } + } + } + + public function loadData() + { + try { + $metrics = $this->resource->getMetrics($this->interval); + $cpuMetrics = collect($metrics)->map(function ($metric) { + return [$metric[0], $metric[1]]; + }); + $memoryMetrics = collect($metrics)->map(function ($metric) { + return [$metric[0], $metric[2]]; + }); + $this->dispatch("refreshChartData-{$this->chartId}-cpu", [ + 'seriesData' => $cpuMetrics, + ]); + $this->dispatch("refreshChartData-{$this->chartId}-memory", [ + 'seriesData' => $memoryMetrics, + ]); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function setInterval() + { + if ($this->interval <= 10) { + $this->poll = true; + } + $this->loadData(); + } + + public function render() + { + return view('livewire.project.shared.metrics'); + } +} diff --git a/app/Livewire/Project/Show.php b/app/Livewire/Project/Show.php index d5d660017..1082f078c 100644 --- a/app/Livewire/Project/Show.php +++ b/app/Livewire/Project/Show.php @@ -9,6 +9,8 @@ class Show extends Component { public Project $project; + public $environments; + public function mount() { $projectUuid = request()->route('project_uuid'); @@ -18,7 +20,8 @@ class Show extends Component if (! $project) { return redirect()->route('dashboard'); } - $project->load(['environments']); + + $this->environments = $project->environments->sortBy('created_at'); $this->project = $project; } diff --git a/app/Livewire/Server/Charts.php b/app/Livewire/Server/Charts.php new file mode 100644 index 000000000..0921c7fa4 --- /dev/null +++ b/app/Livewire/Server/Charts.php @@ -0,0 +1,62 @@ +poll || $this->interval <= 10) { + $this->loadData(); + if ($this->interval > 10) { + $this->poll = false; + } + } + } + + public function loadData() + { + try { + $cpuMetrics = $this->server->getCpuMetrics($this->interval); + $memoryMetrics = $this->server->getMemoryMetrics($this->interval); + $cpuMetrics = collect($cpuMetrics)->map(function ($metric) { + return [$metric[0], $metric[1]]; + }); + $memoryMetrics = collect($memoryMetrics)->map(function ($metric) { + return [$metric[0], $metric[1]]; + }); + $this->dispatch("refreshChartData-{$this->chartId}-cpu", [ + 'seriesData' => $cpuMetrics, + ]); + $this->dispatch("refreshChartData-{$this->chartId}-memory", [ + 'seriesData' => $memoryMetrics, + ]); + + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function setInterval() + { + if ($this->interval <= 10) { + $this->poll = true; + } + $this->loadData(); + } +} diff --git a/app/Livewire/Server/ConfigureCloudflareTunnels.php b/app/Livewire/Server/ConfigureCloudflareTunnels.php index 7d2103e37..f7306a5b5 100644 --- a/app/Livewire/Server/ConfigureCloudflareTunnels.php +++ b/app/Livewire/Server/ConfigureCloudflareTunnels.php @@ -21,7 +21,7 @@ class ConfigureCloudflareTunnels extends Component $server->settings->is_cloudflare_tunnel = true; $server->settings->save(); $this->dispatch('success', 'Cloudflare Tunnels configured successfully.'); - $this->dispatch('serverInstalled'); + $this->dispatch('refreshServerShow'); } catch (\Throwable $e) { return handleError($e, $this); } @@ -37,7 +37,7 @@ class ConfigureCloudflareTunnels extends Component $server->save(); $server->settings->save(); $this->dispatch('success', 'Cloudflare Tunnels configured successfully.'); - $this->dispatch('serverInstalled'); + $this->dispatch('refreshServerShow'); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php index 263ff6367..5616123a5 100644 --- a/app/Livewire/Server/Form.php +++ b/app/Livewire/Server/Form.php @@ -2,6 +2,9 @@ namespace App\Livewire\Server; +use App\Actions\Server\StartSentinel; +use App\Actions\Server\StopSentinel; +use App\Jobs\PullSentinelImageJob; use App\Models\Server; use Livewire\Component; @@ -36,7 +39,12 @@ class Form extends Component 'server.settings.is_build_server' => 'required|boolean', 'server.settings.concurrent_builds' => 'required|integer|min:1', 'server.settings.dynamic_timeout' => 'required|integer|min:1', + 'server.settings.is_metrics_enabled' => 'required|boolean', + 'server.settings.metrics_token' => 'required', + 'server.settings.metrics_refresh_rate_seconds' => 'required|integer|min:1', + 'server.settings.metrics_history_days' => 'required|integer|min:1', 'wildcard_domain' => 'nullable|url', + 'server.settings.is_server_api_enabled' => 'required|boolean', ]; protected $validationAttributes = [ @@ -52,7 +60,11 @@ class Form extends Component 'server.settings.is_build_server' => 'Build Server', 'server.settings.concurrent_builds' => 'Concurrent Builds', 'server.settings.dynamic_timeout' => 'Dynamic Timeout', - + 'server.settings.is_metrics_enabled' => 'Metrics', + 'server.settings.metrics_token' => 'Metrics Token', + 'server.settings.metrics_refresh_rate_seconds' => 'Metrics Interval', + 'server.settings.metrics_history_days' => 'Metrics History', + 'server.settings.is_server_api_enabled' => 'Server API', ]; public function mount() @@ -69,18 +81,59 @@ class Form extends Component public function updatedServerSettingsIsBuildServer() { - $this->dispatch('serverInstalled'); + $this->dispatch('refreshServerShow'); $this->dispatch('serverRefresh'); $this->dispatch('proxyStatusUpdated'); } + public function checkPortForServerApi() + { + try { + if ($this->server->settings->is_server_api_enabled === true) { + $this->server->checkServerApi(); + $this->dispatch('success', 'Server API is reachable.'); + } + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function instantSave() { try { refresh_server_connection($this->server->privateKey); $this->validateServer(false); $this->server->settings->save(); + $this->server->save(); $this->dispatch('success', 'Server updated.'); + $this->dispatch('refreshServerShow'); + if ($this->server->isSentinelEnabled()) { + PullSentinelImageJob::dispatchSync($this->server); + ray('Sentinel is enabled'); + if ($this->server->settings->isDirty('is_metrics_enabled')) { + $this->dispatch('reloadWindow'); + } + if ($this->server->settings->isDirty('is_server_api_enabled') && $this->server->settings->is_server_api_enabled === true) { + ray('Starting sentinel'); + + } + } else { + ray('Sentinel is not enabled'); + StopSentinel::dispatch($this->server); + } + // $this->checkPortForServerApi(); + + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function restartSentinel() + { + try { + $version = get_latest_sentinel_version(); + StartSentinel::run($this->server, $version, true); + $this->dispatch('success', 'Sentinel restarted.'); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index 7ebf90115..0751b186e 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -14,7 +14,7 @@ class Show extends Component public $parameters = []; - protected $listeners = ['serverInstalled' => '$refresh']; + protected $listeners = ['refreshServerShow' => '$refresh']; public function mount() { diff --git a/app/Livewire/Server/ValidateAndInstall.php b/app/Livewire/Server/ValidateAndInstall.php index bd33937e0..422cae779 100644 --- a/app/Livewire/Server/ValidateAndInstall.php +++ b/app/Livewire/Server/ValidateAndInstall.php @@ -143,7 +143,8 @@ class ValidateAndInstall extends Component } else { $this->docker_version = $this->server->validateDockerEngineVersion(); if ($this->docker_version) { - $this->dispatch('serverInstalled'); + $this->dispatch('refreshServerShow'); + $this->dispatch('refreshBoardingIndex'); $this->dispatch('success', 'Server validated.'); } else { $this->error = 'Docker Engine version is not 22+. Please install Docker manually before continuing: documentation.'; diff --git a/app/Livewire/Settings/Configuration.php b/app/Livewire/Settings/Configuration.php index 4dfa16e30..3b6d7cd72 100644 --- a/app/Livewire/Settings/Configuration.php +++ b/app/Livewire/Settings/Configuration.php @@ -29,6 +29,7 @@ class Configuration extends Component 'settings.public_port_min' => 'required', 'settings.public_port_max' => 'required', 'settings.custom_dns_servers' => 'nullable', + 'settings.instance_name' => 'nullable', ]; protected $validationAttributes = [ diff --git a/app/Models/Application.php b/app/Models/Application.php index 6e55f6626..50c7760ff 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -228,18 +228,13 @@ class Application extends BaseModel public function gitCommitLink($link): string { - if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) { + if (! is_null(data_get($this, 'source.html_url')) && ! is_null(data_get($this, 'git_repository')) && ! is_null(data_get($this, 'git_branch'))) { if (str($this->source->html_url)->contains('bitbucket')) { return "{$this->source->html_url}/{$this->git_repository}/commits/{$link}"; } return "{$this->source->html_url}/{$this->git_repository}/commit/{$link}"; } - if (strpos($this->git_repository, 'git@') === 0) { - $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository); - - return "https://{$git_repository}/commit/{$link}"; - } if (str($this->git_repository)->contains('bitbucket')) { $git_repository = str_replace('.git', '', $this->git_repository); $url = Url::fromString($git_repository); @@ -248,6 +243,14 @@ class Application extends BaseModel return $url->__toString(); } + if (strpos($this->git_repository, 'git@') === 0) { + $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository); + if (data_get($this, 'source.html_url')) { + return "{$this->source->html_url}/{$git_repository}/commit/{$link}"; + } + + return "{$git_repository}/commit/{$link}"; + } return $this->git_repository; } @@ -532,7 +535,7 @@ class Application extends BaseModel public function get_last_successful_deployment() { - return ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'finished')->where('pull_request_id', 0)->orderBy('created_at', 'desc')->first(); + return ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', ApplicationDeploymentStatus::FINISHED)->where('pull_request_id', 0)->orderBy('created_at', 'desc')->first(); } public function get_last_days_deployments() @@ -1167,4 +1170,44 @@ class Application extends BaseModel return $preview; } + + public static function getDomainsByUuid(string $uuid): array + { + $application = self::where('uuid', $uuid)->first(); + + if ($application) { + return $application->fqdns; + } + + return []; + } + + public function getMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/Environment.php b/app/Models/Environment.php index e84b6989b..b2bb51092 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -109,7 +109,7 @@ class Environment extends Model protected function name(): Attribute { return Attribute::make( - set: fn (string $value) => strtolower($value), + set: fn (string $value) => str($value)->lower()->trim()->replace('/', '-')->toString(), ); } } diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php index 452c5ca22..38f79ce75 100644 --- a/app/Models/InstanceSettings.php +++ b/app/Models/InstanceSettings.php @@ -47,4 +47,14 @@ class InstanceSettings extends Model implements SendsEmail return explode(',', $recipients); } + + public function getTitleDisplayName(): string + { + $instanceName = $this->instance_name; + if (! $instanceName) { + return ''; + } + + return "[{$instanceName}]"; + } } diff --git a/app/Models/Project.php b/app/Models/Project.php index acc98e341..1cbce6cac 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -112,4 +112,14 @@ class Project extends BaseModel { return $this->postgresqls()->get()->merge($this->redis()->get())->merge($this->mongodbs()->get())->merge($this->mysqls()->get())->merge($this->mariadbs()->get())->merge($this->keydbs()->get())->merge($this->dragonflies()->get())->merge($this->clickhouses()->get()); } + + public function default_environment() + { + $default = $this->environments()->where('name', 'production')->first(); + if (! $default) { + $default = $this->environments()->sortBy('created_at')->first(); + } + + return $default; + } } diff --git a/app/Models/Server.php b/app/Models/Server.php index b1419dc0e..7a99940fd 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -5,12 +5,11 @@ namespace App\Models; use App\Actions\Server\InstallDocker; use App\Enums\ProxyTypes; use App\Jobs\PullSentinelImageJob; -use App\Notifications\Server\Revived; -use App\Notifications\Server\Unreachable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Illuminate\Support\Stringable; @@ -462,10 +461,44 @@ $schema://$host { Storage::disk('ssh-mux')->delete($this->muxFilename()); } + public function isSentinelEnabled() + { + return $this->isMetricsEnabled() || $this->isServerApiEnabled(); + } + + public function isMetricsEnabled() + { + return $this->settings->is_metrics_enabled; + } + + public function isServerApiEnabled() + { + return $this->settings->is_server_api_enabled; + } + + public function checkServerApi() + { + if ($this->isServerApiEnabled()) { + $server_ip = $this->ip; + if (isDev()) { + if ($this->id === 0) { + $server_ip = 'localhost'; + } + } + $command = "curl -s http://{$server_ip}:12172/api/health"; + $process = Process::timeout(5)->run($command); + if ($process->failed()) { + ray($process->exitCode(), $process->output(), $process->errorOutput()); + throw new \Exception("Server API is not reachable on http://{$server_ip}:12172"); + } + + } + } + public function checkSentinel() { ray("Checking sentinel on server: {$this->name}"); - if ($this->is_metrics_enabled) { + if ($this->isSentinelEnabled()) { $sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false); $sentinel_found = json_decode($sentinel_found, true); $status = data_get($sentinel_found, '0.State.Status', 'exited'); @@ -478,21 +511,57 @@ $schema://$host { } } - public function getMetrics() + public function getCpuMetrics(int $mins = 5) { - if ($this->is_metrics_enabled) { - $from = now()->subMinutes(5)->toIso8601ZuluString(); - $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl http://localhost:8888/api/cpu/history?from=$from'"], $this, false); + if ($this->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->metrics_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false); + if (str($cpu)->contains('error')) { + $error = json_decode($cpu, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } $cpu = str($cpu)->explode("\n")->skip(1)->all(); $parsedCollection = collect($cpu)->flatMap(function ($item) { return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $value] = explode(',', trim($line)); + [$time, $cpu_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 0); - return [(int) $time, (float) $value]; + return [(int) $time, (float) $cpu_usage_percent]; }); - })->toArray(); + }); - return $parsedCollection; + return $parsedCollection->toArray(); + } + } + + public function getMemoryMetrics(int $mins = 5) + { + if ($this->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->metrics_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false); + if (str($memory)->contains('error')) { + $error = json_decode($memory, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $memory = str($memory)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($memory)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $used, $free, $usedPercent] = explode(',', trim($line)); + $usedPercent = number_format($usedPercent, 0); + + return [(int) $time, (float) $usedPercent]; + }); + }); + + return $parsedCollection->toArray(); } } diff --git a/app/Models/Service.php b/app/Models/Service.php index 7851eb58a..c608e38dd 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Collection; +use Symfony\Component\Yaml\Yaml; class Service extends BaseModel { @@ -837,14 +838,34 @@ class Service extends BaseModel $commands[] = "mkdir -p $workdir"; $commands[] = "cd $workdir"; + $json = Yaml::parse($this->docker_compose); + $envs_from_coolify = $this->environment_variables()->get(); + foreach ($json['services'] as $service => $config) { + $envs = collect($config['environment']); + $envs->push("COOLIFY_CONTAINER_NAME=$service-{$this->uuid}"); + foreach ($envs_from_coolify as $env) { + $envs = $envs->map(function ($value) use ($env) { + if (str($value)->startsWith($env->key)) { + return "{$env->key}={$env->real_value}"; + } + + return $value; + }); + } + $envs = $envs->unique(); + data_set($json, "services.$service.environment", $envs->toArray()); + } + + $this->docker_compose = Yaml::dump($json, 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); $docker_compose_base64 = base64_encode($this->docker_compose); + $commands[] = "echo $docker_compose_base64 | base64 -d | tee docker-compose.yml > /dev/null"; - $envs = $this->environment_variables()->get(); $commands[] = 'rm -f .env || true'; - foreach ($envs as $env) { + + foreach ($envs_from_coolify as $env) { $commands[] = "echo '{$env->key}={$env->real_value}' >> .env"; } - if ($envs->count() === 0) { + if ($envs_from_coolify->count() === 0) { $commands[] = 'touch .env'; } instant_remote_process($commands, $this->server); diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index c5e252c34..e968db18d 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -226,4 +226,33 @@ class StandaloneClickhouse extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index 8c739d984..c6718acfe 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -226,4 +226,33 @@ class StandaloneDragonfly extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 5216681c9..142f960aa 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -226,4 +226,33 @@ class StandaloneKeydb extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index 33fd2cbc2..7e6d2e0d1 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -226,4 +226,33 @@ class StandaloneMariadb extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 0cc52b3f7..df895bb34 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -246,4 +246,33 @@ class StandaloneMongodb extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index 174736f77..bd160f877 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -227,4 +227,33 @@ class StandaloneMysql extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index a5bf4dc2a..114d376e8 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -227,4 +227,33 @@ class StandalonePostgresql extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index ed379750e..022cd8d09 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -222,4 +222,33 @@ class StandaloneRedis extends BaseModel { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Notifications/Server/Revived.php b/app/Notifications/Server/Revived.php index e7d3baf3e..8eaadf359 100644 --- a/app/Notifications/Server/Revived.php +++ b/app/Notifications/Server/Revived.php @@ -24,7 +24,7 @@ class Revived extends Notification implements ShouldQueue if ($this->server->unreachable_notification_sent === false) { return; } - GetContainersStatus::dispatch($server); + GetContainersStatus::dispatch($server)->onQueue('high'); // dispatch(new ContainerStatusJob($server)); } diff --git a/app/View/Components/Forms/Textarea.php b/app/View/Components/Forms/Textarea.php index bfdf03a31..7d1860500 100644 --- a/app/View/Components/Forms/Textarea.php +++ b/app/View/Components/Forms/Textarea.php @@ -19,6 +19,8 @@ class Textarea extends Component public ?string $value = null, public ?string $label = null, public ?string $placeholder = null, + public ?string $monacoEditorLanguage = '', + public bool $useMonacoEditor = false, public bool $required = false, public bool $disabled = false, public bool $readonly = false, diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php index 999de45c2..c278a5045 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -1,5 +1,7 @@ user()->currentAccessToken(); @@ -10,3 +12,27 @@ function invalid_token() { return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api-reference/authorization'], 400); } + +function serialize_api_response($data) +{ + if (! $data instanceof Collection) { + $data = collect($data); + } + $data = $data->sortKeys(); + $created_at = data_get($data, 'created_at'); + $updated_at = data_get($data, 'updated_at'); + if ($created_at) { + unset($data['created_at']); + $data['created_at'] = $created_at; + + } + if ($updated_at) { + unset($data['updated_at']); + $data['updated_at'] = $updated_at; + } + if (data_get($data, 'id')) { + $data = $data->prepend($data['id'], 'id'); + } + + return $data; +} diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index 376b0f2aa..df891b824 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -8,7 +8,7 @@ use App\Models\Server; use App\Models\StandaloneDocker; use Spatie\Url\Url; -function queue_application_deployment(Application $application, string $deployment_uuid, ?int $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, ?Server $server = null, ?StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false) +function queue_application_deployment(Application $application, string $deployment_uuid, ?int $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $is_api = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, ?Server $server = null, ?StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false) { $application_id = $application->id; $deployment_link = Url::fromString($application->link()."/deployment/{$deployment_uuid}"); @@ -35,6 +35,7 @@ function queue_application_deployment(Application $application, string $deployme 'pull_request_id' => $pull_request_id, 'force_rebuild' => $force_rebuild, 'is_webhook' => $is_webhook, + 'is_api' => $is_api, 'restart_only' => $restart_only, 'commit' => $commit, 'rollback' => $rollback, @@ -65,7 +66,7 @@ function force_start_deployment(ApplicationDeploymentQueue $deployment) function queue_next_deployment(Application $application) { $server_id = $application->destination->server_id; - $next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', 'queued')->get()->sortBy('created_at')->first(); + $next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', ApplicationDeploymentStatus::QUEUED)->get()->sortBy('created_at')->first(); if ($next_found) { $next_found->update([ 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, @@ -79,7 +80,7 @@ function queue_next_deployment(Application $application) function next_queuable(string $server_id, string $application_id): bool { - $deployments = ApplicationDeploymentQueue::where('server_id', $server_id)->whereIn('status', ['in_progress', 'queued'])->get()->sortByDesc('created_at'); + $deployments = ApplicationDeploymentQueue::where('server_id', $server_id)->whereIn('status', ['in_progress', ApplicationDeploymentStatus::QUEUED])->get()->sortByDesc('created_at'); $same_application_deployments = $deployments->where('application_id', $application_id); $in_progress = $same_application_deployments->filter(function ($value, $key) { return $value->status === 'in_progress'; @@ -98,3 +99,26 @@ function next_queuable(string $server_id, string $application_id): bool return true; } +function next_after_cancel(?Server $server = null) +{ + if ($server) { + $next_found = ApplicationDeploymentQueue::where('server_id', data_get($server, 'id'))->where('status', ApplicationDeploymentStatus::QUEUED)->get()->sortBy('created_at'); + if ($next_found->count() > 0) { + foreach ($next_found as $next) { + $server = Server::find($next->server_id); + $concurrent_builds = $server->settings->concurrent_builds; + $inprogress_deployments = ApplicationDeploymentQueue::where('server_id', $next->server_id)->whereIn('status', [ApplicationDeploymentStatus::QUEUED])->get()->sortByDesc('created_at'); + if ($inprogress_deployments->count() < $concurrent_builds) { + $next->update([ + 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, + ]); + + dispatch(new ApplicationDeploymentJob( + application_deployment_queue_id: $next->id, + )); + } + break; + } + } + } +} diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 7994c10af..aef362491 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -79,6 +79,10 @@ function backup_dir(): string { return base_configuration_dir().'/backups'; } +function metrics_dir(): string +{ + return base_configuration_dir().'/metrics'; +} function generate_readme_file(string $name, string $updated_at): string { @@ -158,10 +162,10 @@ function get_route_parameters(): array function get_latest_sentinel_version(): string { try { - $response = Http::get('https://cdn.coollabs.io/coolify/versions.json'); + $response = Http::get('https://cdn.coollabs.io/sentinel/versions.json'); $versions = $response->json(); - return data_get($versions, 'coolify.sentinel.version'); + return data_get($versions, 'sentinel.version'); } catch (\Throwable $e) { //throw $e; ray($e->getMessage()); @@ -1250,6 +1254,18 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal ]); } } + $envs_from_coolify = $resource->environment_variables()->get(); + $serviceVariables = $serviceVariables->map(function ($variable) use ($envs_from_coolify) { + $env_variable_key = str($variable)->before('='); + $env_variable_value = str($variable)->after('='); + $found_env = $envs_from_coolify->where('key', $env_variable_key)->first(); + if ($found_env) { + $env_variable_value = $found_env->value; + } + + return "$env_variable_key=$env_variable_value"; + }); + } // Add labels to the service if ($savedService->serviceType()) { @@ -1314,19 +1330,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal data_forget($service, 'volumes.*.isDirectory'); data_forget($service, 'volumes.*.is_directory'); data_forget($service, 'exclude_from_hc'); - - // Remove unnecessary variables from service.environment - // $withoutServiceEnvs = collect([]); - // collect(data_get($service, 'environment'))->each(function ($value, $key) use ($withoutServiceEnvs) { - // ray($key, $value); - // if (!Str::of($key)->startsWith('$SERVICE_') && !Str::of($value)->startsWith('SERVICE_')) { - // $k = Str::of($value)->before("="); - // $v = Str::of($value)->after("="); - // $withoutServiceEnvs->put($k->value(), $v->value()); - // } - // }); - // ray($withoutServiceEnvs); - // data_set($service, 'environment', $withoutServiceEnvs->toArray()); + data_set($service, 'environment', $serviceVariables->toArray()); updateCompose($savedService); return $service; @@ -2282,3 +2286,10 @@ function isAnyDeploymentInprogress() echo "No deployments in progress.\n"; exit(0); } + +function generateSentinelToken() +{ + $token = Str::random(64); + + return $token; +} diff --git a/config/constants.php b/config/constants.php index 444d144a8..861b645ed 100644 --- a/config/constants.php +++ b/config/constants.php @@ -22,8 +22,8 @@ return [ ], 'services' => [ // Temporary disabled until cache is implemented - 'official' => 'https://cdn.coollabs.io/coolify/service-templates.json', - // 'official' => 'https://raw.githubusercontent.com/coollabsio/coolify/main/templates/service-templates.json', + // 'official' => 'https://cdn.coollabs.io/coolify/service-templates.json', + 'official' => 'https://raw.githubusercontent.com/coollabsio/coolify/main/templates/service-templates.json', ], 'limits' => [ 'trial_period' => 0, diff --git a/config/coolify.php b/config/coolify.php index c7cfe6101..a6d6d8581 100644 --- a/config/coolify.php +++ b/config/coolify.php @@ -14,5 +14,4 @@ return [ 'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper:latest'), 'is_horizon_enabled' => env('HORIZON_ENABLED', true), 'is_scheduler_enabled' => env('SCHEDULER_ENABLED', true), - 'is_sentinel_enabled' => env('SENTINEL_ENABLED', false), ]; diff --git a/config/horizon.php b/config/horizon.php index ef7df3f1b..939d74883 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -182,7 +182,7 @@ return [ 'defaults' => [ 's6' => [ 'connection' => 'redis', - 'queue' => ['default'], + 'queue' => ['high', 'default'], 'balance' => env('HORIZON_BALANCE', 'auto'), 'maxTime' => 0, 'maxJobs' => 0, diff --git a/config/sentry.php b/config/sentry.php index 33a24edfb..caa659921 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ return [ // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => '4.0.0-beta.297', + 'release' => '4.0.0-beta.298', // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/session.php b/config/session.php index c7b176a5a..44ca7ded9 100644 --- a/config/session.php +++ b/config/session.php @@ -18,7 +18,7 @@ return [ | */ - 'driver' => env('SESSION_DRIVER', 'redis'), + 'driver' => env('SESSION_DRIVER', 'database'), /* |-------------------------------------------------------------------------- diff --git a/config/version.php b/config/version.php index 06c1e6c66..ddcd3f2d4 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ dropColumn('is_metrics_enabled'); + }); + Schema::table('server_settings', function (Blueprint $table) { + $table->boolean('is_metrics_enabled')->default(false); + $table->integer('metrics_refresh_rate_seconds')->default(5); + $table->integer('metrics_history_days')->default(30); + $table->string('metrics_token')->default(generateSentinelToken()); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->boolean('is_metrics_enabled')->default(true); + }); + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('is_metrics_enabled'); + $table->dropColumn('metrics_refresh_rate_seconds'); + $table->dropColumn('metrics_history_days'); + $table->dropColumn('metrics_token'); + }); + } +}; diff --git a/database/migrations/2024_06_20_102551_add_server_api_sentinel.php b/database/migrations/2024_06_20_102551_add_server_api_sentinel.php new file mode 100644 index 000000000..b840195af --- /dev/null +++ b/database/migrations/2024_06_20_102551_add_server_api_sentinel.php @@ -0,0 +1,28 @@ +boolean('is_server_api_enabled')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('is_server_api_enabled'); + }); + } +}; diff --git a/database/migrations/2024_06_21_143358_add_api_deployment_type.php b/database/migrations/2024_06_21_143358_add_api_deployment_type.php new file mode 100644 index 000000000..03f4d4796 --- /dev/null +++ b/database/migrations/2024_06_21_143358_add_api_deployment_type.php @@ -0,0 +1,28 @@ +boolean('is_api')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_deployment_queues', function (Blueprint $table) { + $table->dropColumn('is_api'); + }); + } +}; diff --git a/database/migrations/2024_06_22_081140_alter_instance_settings_add_instance_name.php b/database/migrations/2024_06_22_081140_alter_instance_settings_add_instance_name.php new file mode 100644 index 000000000..1687e047c --- /dev/null +++ b/database/migrations/2024_06_22_081140_alter_instance_settings_add_instance_name.php @@ -0,0 +1,28 @@ +string('instance_name')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('instance_settings', function (Blueprint $table) { + $table->dropColumn('instance_name'); + }); + } +}; diff --git a/lang/ar.json b/lang/ar.json new file mode 100644 index 000000000..c5ec96c8d --- /dev/null +++ b/lang/ar.json @@ -0,0 +1,30 @@ +{ + "auth.login": "تسجيل الدخول", + "auth.login.azure": "تسجيل الدخول باستخدام Microsoft", + "auth.login.bitbucket": "تسجيل الدخول باستخدام Bitbucket", + "auth.login.github": "تسجيل الدخول باستخدام GitHub", + "auth.login.gitlab": "تسجيل الدخول باستخدام Gitlab", + "auth.login.google": "تسجيل الدخول باستخدام Google", + "auth.already_registered": "هل سبق لك التسجيل؟", + "auth.confirm_password": "تأكيد كلمة المرور", + "auth.forgot_password": "نسيت كلمة المرور", + "auth.forgot_password_send_email": "إرسال بريد إلكتروني لإعادة تعيين كلمة المرور", + "auth.register_now": "تسجيل", + "auth.logout": "تسجيل الخروج", + "auth.register": "تسجيل", + "auth.registration_disabled": "تم تعطيل التسجيل. يرجى التواصل مع المسؤول.", + "auth.reset_password": "إعادة تعيين كلمة المرور", + "auth.failed": "هذه البيانات لا تتطابق مع سجلاتنا.", + "auth.failed.callback": "فشل في معالجة استدعاء من مزود تسجيل الدخول.", + "auth.failed.password": "كلمة المرور المقدمة غير صحيحة.", + "auth.failed.email": "لا يمكننا العثور على مستخدم بهذا البريد الإلكتروني.", + "auth.throttle": "عدد محاولات تسجيل الدخول كثيرة جدًا. يرجى المحاولة مرة أخرى في :seconds ثانية.", + "input.name": "الاسم", + "input.email": "البريد الإلكتروني", + "input.password": "كلمة المرور", + "input.password.again": "كلمة المرور مرة أخرى", + "input.code": "الرمز لمرة واحدة", + "input.recovery_code": "رمز الاسترداد", + "button.save": "حفظ", + "repository.url": "أمثلة
للمستودعات العامة، استخدم https://....
للمستودعات الخاصة، استخدم git@....

سيتم تحديد الفرع main لـ https://github.com/coollabsio/coolify-examples
سيتم تحديد الفرع nodejs-fastify لـ https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify
سيتم تحديد الفرع main لـ https://gitea.com/sedlav/expressjs.git
سيتم تحديد الفرع main لـ https://gitlab.com/andrasbacsai/nodejs-example.git." +} diff --git a/lang/de.json b/lang/de.json new file mode 100644 index 000000000..29fec629f --- /dev/null +++ b/lang/de.json @@ -0,0 +1,30 @@ +{ + "auth.login": "Anmelden", + "auth.login.azure": "Mit Microsoft anmelden", + "auth.login.bitbucket": "Mit Bitbucket anmelden", + "auth.login.github": "Mit GitHub anmelden", + "auth.login.gitlab": "Mit GitLab anmelden", + "auth.login.google": "Mit Google anmelden", + "auth.already_registered": "Bereits registriert?", + "auth.confirm_password": "Passwort bestätigen", + "auth.forgot_password": "Passwort vergessen", + "auth.forgot_password_send_email": "Passwort zurücksetzen E-Mail senden", + "auth.register_now": "Registrieren", + "auth.logout": "Abmelden", + "auth.register": "Registrieren", + "auth.registration_disabled": "Registrierung ist deaktiviert. Bitte kontaktiere einen Administrator.", + "auth.reset_password": "Passwort zurücksetzen", + "auth.failed": "Diese Anmeldedaten wurden nicht gefunden.", + "auth.failed.callback": "Fehlerhafte Verarbeitung der Antwort des Anmeldeanbieters.", + "auth.failed.password": "Das angegebene Passwort ist inkorrekt.", + "auth.failed.email": "Wir können keinen Benutzer mit dieser E-Mail Adresse finden.", + "auth.throttle": "Zu viele Anmeldeversuche. Bitte versuche es in :seconds Sekunden erneut.", + "input.name": "Name", + "input.email": "E-Mail", + "input.password": "Passwort", + "input.password.again": "Passwort wiederholen", + "input.code": "Einmalcode", + "input.recovery_code": "Wiederherstellungscode", + "button.save": "Speichern", + "repository.url": "Beispiele
Für öffentliche Repositories benutze https://....
Für private Repositories benutze git@....

https://github.com/coollabsio/coolify-examples main Branch wird ausgewählt
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify Branch wird ausgewählt.
https://gitea.com/sedlav/expressjs.git main Branch wird ausgewählt.
https://gitlab.com/andrasbacsai/nodejs-example.git main Branch wird ausgewählt." +} diff --git a/lang/es.json b/lang/es.json new file mode 100644 index 000000000..0d8c0c940 --- /dev/null +++ b/lang/es.json @@ -0,0 +1,30 @@ +{ + "auth.login": "Iniciar Sesión", + "auth.login.azure": "Acceder con Microsoft", + "auth.login.bitbucket": "Acceder con Bitbucket", + "auth.login.github": "Acceder con GitHub", + "auth.login.gitlab": "Acceder con Gitlab", + "auth.login.google": "Acceder con Google", + "auth.already_registered": "¿Ya estás registrado?", + "auth.confirm_password": "Confirmar contraseña", + "auth.forgot_password": "¿Olvidaste tu contraseña?", + "auth.forgot_password_send_email": "Enviar correo de recuperación de contraseña", + "auth.register_now": "Registrar", + "auth.logout": "Cerrar sesión", + "auth.register": "Registrar", + "auth.registration_disabled": "El registro está desactivado. Por favor contacta con el administrador.", + "auth.reset_password": "Cambiar contraseña", + "auth.failed": "Las credenciales no coinciden con nuestro registro..", + "auth.failed.callback": "Falló el proceso de inicio de sesión con el proveedor.", + "auth.failed.password": "La contraseña es incorrecta.", + "auth.failed.email": "No encontramos un usuario con ese correo.", + "auth.throttle": "Demasiados intentos. Por favor inténtalo en :seconds segundos.", + "input.name": "Nombre", + "input.email": "Correo", + "input.password": "Contraseña", + "input.password.again": "Escribe la contraseña otra vez", + "input.code": "Código de único uso", + "input.recovery_code": "Código de recuperación", + "button.save": "Guardar", + "repository.url": "Examples
Para repositorios públicos, usar https://....
Para repositorios privados, usar git@....

https://github.com/coollabsio/coolify-examples main la rama 'main' será seleccionada.
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify la rama 'nodejs-fastify' será seleccionada.
https://gitea.com/sedlav/expressjs.git main la rama 'main' será seleccionada.
https://gitlab.com/andrasbacsai/nodejs-example.git main la rama 'main' será seleccionada." +} \ No newline at end of file diff --git a/lang/fr.json b/lang/fr.json new file mode 100644 index 000000000..ae7fa0a03 --- /dev/null +++ b/lang/fr.json @@ -0,0 +1,30 @@ +{ + "auth.login": "Connexion", + "auth.login.azure": "Connexion avec Microsoft", + "auth.login.bitbucket": "Connexion avec Bitbucket", + "auth.login.github": "Connexion avec GitHub", + "auth.login.gitlab": "Connexion avec Gitlab", + "auth.login.google": "Connexion avec Google", + "auth.already_registered": "Déjà enregistré ?", + "auth.confirm_password": "Confirmer le mot de passe", + "auth.forgot_password": "Mot de passe oublié", + "auth.forgot_password_send_email": "Envoyer l'email de réinitialisation de mot de passe", + "auth.register_now": "S'enregistrer", + "auth.logout": "Déconnexion", + "auth.register": "S'enregistrer", + "auth.registration_disabled": "L'enregistrement est désactivé. Merci de contacter l'administateur.", + "auth.reset_password": "Réinitialiser le mot de passe", + "auth.failed": "Aucune correspondance n'a été trouvé pour les informations d'identification renseignées.", + "auth.failed.callback": "Erreur lors du processus de retour de la plateforme de connexion.", + "auth.failed.password": "Le mot de passe renseigné est incorrect.", + "auth.failed.email": "Aucun utilisateur avec cette adresse email n'a été trouvé.", + "auth.throttle": "Trop de tentatives de connexion. Merci de réessayer dans :seconds secondes.", + "input.name": "Nom", + "input.email": "Email", + "input.password": "Mot de passe", + "input.password.again": "Mot de passe identique", + "input.code": "Code à usage unique", + "input.recovery_code": "Code de récupération", + "button.save": "Sauvegarder", + "repository.url": "Exemples
Pour les dépôts publiques, utilisez https://....
Pour les dépôts privés, utilisez git@....

https://github.com/coollabsio/coolify-examples main sera la branche selectionnée
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify sera la branche selectionnée.
https://gitea.com/sedlav/expressjs.git main sera la branche selectionnée.
https://gitlab.com/andrasbacsai/nodejs-example.git main sera la branche selectionnée." +} diff --git a/lang/it.json b/lang/it.json new file mode 100644 index 000000000..6e4feb9cc --- /dev/null +++ b/lang/it.json @@ -0,0 +1,30 @@ +{ + "auth.login": "Accedi", + "auth.login.azure": "Accedi con Microsoft", + "auth.login.bitbucket": "Accedi con Bitbucket", + "auth.login.github": "Accedi con GitHub", + "auth.login.gitlab": "Accedi con Gitlab", + "auth.login.google": "Accedi con Google", + "auth.already_registered": "Già registrato?", + "auth.confirm_password": "Conferma password", + "auth.forgot_password": "Password dimenticata", + "auth.forgot_password_send_email": "Invia email per reimpostare la password", + "auth.register_now": "Registrati", + "auth.logout": "Esci", + "auth.register": "Registrati", + "auth.registration_disabled": "La registrazione è disabilitata. Si prega di contattare l'amministratore.", + "auth.reset_password": "Reimposta password", + "auth.failed": "Queste credenziali non corrispondono ai nostri record.", + "auth.failed.callback": "Errore durante l'elaborazione del callback dal provider di accesso.", + "auth.failed.password": "La password fornita non è corretta.", + "auth.failed.email": "Non possiamo trovare un utente con questo indirizzo email.", + "auth.throttle": "Troppi tentativi di accesso. Per favore riprova tra :seconds secondi.", + "input.name": "Nome", + "input.email": "Email", + "input.password": "Password", + "input.password.again": "Ripeti password", + "input.code": "Codice monouso", + "input.recovery_code": "Codice di recupero", + "button.save": "Salva", + "repository.url": "Esempi
Per i repository pubblici, utilizza https://....
Per i repository privati, utilizza git@....

https://github.com/coollabsio/coolify-examples verrà selezionato il branch main
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify verrà selezionato il branch nodejs-fastify.
https://gitea.com/sedlav/expressjs.git verrà selezionato il branch main.
https://gitlab.com/andrasbacsai/nodejs-example.git verrà selezionato il branch main." +} diff --git a/lang/ja.json b/lang/ja.json new file mode 100644 index 000000000..4652a3b17 --- /dev/null +++ b/lang/ja.json @@ -0,0 +1,30 @@ +{ + "auth.login": "ログイン", + "auth.login.azure": "Microsoftでログイン", + "auth.login.bitbucket": "Bitbucketでログイン", + "auth.login.github": "GitHubでログイン", + "auth.login.gitlab": "Gitlabでログイン", + "auth.login.google": "Googleでログイン", + "auth.already_registered": "すでに登録済みですか?", + "auth.confirm_password": "パスワードを確認", + "auth.forgot_password": "パスワードを忘れた", + "auth.forgot_password_send_email": "パスワードリセットメールを送信", + "auth.register_now": "今すぐ登録", + "auth.logout": "ログアウト", + "auth.register": "登録", + "auth.registration_disabled": "登録は無効です。管理者に連絡してください。", + "auth.reset_password": "パスワードをリセット", + "auth.failed": "これらの資格情報は記録と一致しません。", + "auth.failed.callback": "ログインプロバイダーからのコールバックの処理に失敗しました。", + "auth.failed.password": "提供されたパスワードが正しくありません。", + "auth.failed.email": "そのメールアドレスのユーザーが見つかりません。", + "auth.throttle": "ログイン試行回数が多すぎます。:seconds秒後にもう一度お試しください。", + "input.name": "名前", + "input.email": "メール", + "input.password": "パスワード", + "input.password.again": "パスワード再入力", + "input.code": "ワンタイムコード", + "input.recovery_code": "リカバリーコード", + "button.save": "保存", + "repository.url": "
公開リポジトリの場合はhttps://...を使用してください。
プライベートリポジトリの場合はgit@...を使用してください。

https://github.com/coollabsio/coolify-examples mainブランチが選択されます
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastifyブランチが選択されます。
https://gitea.com/sedlav/expressjs.git mainブランチが選択されます。
https://gitlab.com/andrasbacsai/nodejs-example.git mainブランチが選択されます。" +} diff --git a/lang/pt.json b/lang/pt.json new file mode 100644 index 000000000..b5dd5c434 --- /dev/null +++ b/lang/pt.json @@ -0,0 +1,30 @@ +{ + "auth.login": "Entrar", + "auth.login.azure": "Entrar com Microsoft", + "auth.login.bitbucket": "Entrar com Bitbucket", + "auth.login.github": "Entrar com GitHub", + "auth.login.gitlab": "Entrar com Gitlab", + "auth.login.google": "Entrar com Google", + "auth.already_registered": "Já tem uma conta?", + "auth.confirm_password": "Confirmar senha", + "auth.forgot_password": "Esqueceu a senha?", + "auth.forgot_password_send_email": "Enviar e-mail de redefinição de senha", + "auth.register_now": "Cadastrar-se", + "auth.logout": "Sair", + "auth.register": "Cadastrar", + "auth.registration_disabled": "Cadastro desativado. Por favor, entre em contato com o administrador.", + "auth.reset_password": "Redefinir senha", + "auth.failed": "Essas credenciais não correspondem aos nossos registros.", + "auth.failed.callback": "Falha ao processar o callback do provedor de login.", + "auth.failed.password": "A senha fornecida está incorreta.", + "auth.failed.email": "Não encontramos um usuário com esse endereço de e-mail.", + "auth.throttle": "Muitas tentativas de login. Por favor, tente novamente em :seconds segundos.", + "input.name": "Nome", + "input.email": "E-mail", + "input.password": "Senha", + "input.password.again": "Repetir senha", + "input.code": "Código único", + "input.recovery_code": "Código de recuperação", + "button.save": "Salvar", + "repository.url": "Exemplos
Para repositórios públicos, use https://....
Para repositórios privados, use git@....

https://github.com/coollabsio/coolify-examples a branch main será selecionada
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify a branch nodejs-fastify será selecionada.
https://gitea.com/sedlav/expressjs.git a branch main será selecionada.
https://gitlab.com/andrasbacsai/nodejs-example.git a branch main será selecionada." +} diff --git a/lang/tr.json b/lang/tr.json new file mode 100644 index 000000000..255b0d15b --- /dev/null +++ b/lang/tr.json @@ -0,0 +1,30 @@ +{ + "auth.login": "Giriş", + "auth.login.azure": "Microsoft ile Giriş Yap", + "auth.login.bitbucket": "Bitbucket ile Giriş Yap", + "auth.login.github": "GitHub ile Giriş Yap", + "auth.login.gitlab": "GitLab ile Giriş Yap", + "auth.login.google": "Google ile Giriş Yap", + "auth.already_registered": "Zaten kayıtlı mısınız?", + "auth.confirm_password": "Şifreyi Onayla", + "auth.forgot_password": "Şifremi Unuttum", + "auth.forgot_password_send_email": "Şifre sıfırlama e-postası gönder", + "auth.register_now": "Kayıt Ol", + "auth.logout": "Çıkış Yap", + "auth.register": "Kayıt Ol", + "auth.registration_disabled": "Kayıt devre dışı bırakıldı. Lütfen yöneticiyle iletişime geçin.", + "auth.reset_password": "Şifreyi Sıfırla", + "auth.failed": "Bu kimlik bilgileri kayıtlarımızla eşleşmiyor.", + "auth.failed.callback": "Giriş sağlayıcıdan gelen istek işlenemedi.", + "auth.failed.password": "Sağlanan şifre yanlış.", + "auth.failed.email": "Bu e-posta adresiyle bir kullanıcı bulamıyoruz.", + "auth.throttle": "Çok fazla giriş denemesi. Lütfen :seconds saniye sonra tekrar deneyin.", + "input.name": "İsim", + "input.email": "E-posta", + "input.password": "Şifre", + "input.password.again": "Şifreyi Tekrar Girin", + "input.code": "Tek Kullanımlık Kod", + "input.recovery_code": "Kurtarma Kodu", + "button.save": "Kaydet", + "repository.url": "Örnekler
Halka açık depolar için https://... kullanın.
Özel depolar için git@... kullanın.

https://github.com/coollabsio/coolify-examples main dalı seçilecek
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify dalı seçilecek.
https://gitea.com/sedlav/expressjs.git main dalı seçilecek.
https://gitlab.com/andrasbacsai/nodejs-example.git main dalı seçilecek." +} diff --git a/lang/vi.json b/lang/vi.json new file mode 100644 index 000000000..548dbe8b7 --- /dev/null +++ b/lang/vi.json @@ -0,0 +1,30 @@ +{ + "auth.login": "Đăng Nhập", + "auth.login.azure": "Đăng Nhập Bằng Microsoft", + "auth.login.bitbucket": "Đăng Nhập Bằng Bitbucket", + "auth.login.github": "Đăng Nhập Bằng GitHub", + "auth.login.gitlab": "Đăng Nhập Bằng Gitlab", + "auth.login.google": "Đăng Nhập Bằng Google", + "auth.already_registered": "Đã đăng ký?", + "auth.confirm_password": "Nhập lại mật khẩu", + "auth.forgot_password": "Quên mật khẩu", + "auth.forgot_password_send_email": "Gửi email đặt lại mật khẩu", + "auth.register_now": "Đăng ký ngay", + "auth.logout": "Đăng xuất", + "auth.register": "Đăng ký", + "auth.registration_disabled": "Đăng ký không khả dụng. Vui lòng liên hệ quản trị viên.", + "auth.reset_password": "Đặt lại mật khẩu", + "auth.failed": "Thông tin đăng nhập không khớp với bất kỳ tài khoản nào.", + "auth.failed.callback": "Xử lý thông tin từ nhà cung cấp đăng nhập thất bại.", + "auth.failed.password": "Mật khẩu bạn cung cấp không chính xác.", + "auth.failed.email": "Không có người dùng nào đã đăng ký với email đó.", + "auth.throttle": "Quá nhiều lần đăng nhập thất bại. Vui lòng thử lại sau :seconds giây.", + "input.name": "Tên", + "input.email": "Email", + "input.password": "Mật khẩu", + "input.password.again": "Mật khẩu lần nữa", + "input.code": "One-time code", + "input.recovery_code": "Mã khôi phục", + "button.save": "Lưu", + "repository.url": "Ví dụ
Với repo công khai, sử dụng https://....
Với repo riêng tư, sử dụng git@....

https://github.com/coollabsio/coolify-examples nhánh chính sẽ được chọn
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nhánh nodejs-fastify sẽ được chọn.
https://gitea.com/sedlav/expressjs.git nhánh chính sẽ được chọn.
https://gitlab.com/andrasbacsai/nodejs-example.git nhánh chính sẽ được chọn." +} diff --git a/other/logos/advin.png b/other/logos/advin.png new file mode 100644 index 000000000..155408b9c Binary files /dev/null and b/other/logos/advin.png differ diff --git a/other/logos/blacksmith.png b/other/logos/blacksmith.png new file mode 100644 index 000000000..ee2ed9ee4 Binary files /dev/null and b/other/logos/blacksmith.png differ diff --git a/other/logos/codext.jpg b/other/logos/codext.jpg new file mode 100644 index 000000000..8abf63972 Binary files /dev/null and b/other/logos/codext.jpg differ diff --git a/other/logos/fractal.png b/other/logos/fractal.png new file mode 100644 index 000000000..c4d39c1f1 Binary files /dev/null and b/other/logos/fractal.png differ diff --git a/other/logos/fractal.svg b/other/logos/fractal.svg new file mode 100644 index 000000000..cd2ee4134 --- /dev/null +++ b/other/logos/fractal.svg @@ -0,0 +1,40 @@ + + + + + + Networks + Fractal + + + + + + + \ No newline at end of file diff --git a/other/logos/supaguide.png b/other/logos/supaguide.png new file mode 100644 index 000000000..195f3ce92 Binary files /dev/null and b/other/logos/supaguide.png differ diff --git a/other/logos/tigris.svg b/other/logos/tigris.svg new file mode 100644 index 000000000..367c59f2d --- /dev/null +++ b/other/logos/tigris.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/other/logos/trieve_bg.png b/other/logos/trieve_bg.png new file mode 100644 index 000000000..5425fbd73 Binary files /dev/null and b/other/logos/trieve_bg.png differ diff --git a/package-lock.json b/package-lock.json index 0010d87fa..bec5a7f66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,9 +19,9 @@ "laravel-vite-plugin": "0.8.1", "postcss": "8.4.38", "pusher-js": "8.4.0-rc2", - "tailwindcss": "3.4.3", + "tailwindcss": "3.4.4", "vite": "4.5.3", - "vue": "3.4.27" + "vue": "3.4.29" } }, "node_modules/@alloc/quick-lru": { @@ -36,9 +36,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -535,51 +535,51 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.27.tgz", - "integrity": "sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.29.tgz", + "integrity": "sha512-TFKiRkKKsRCKvg/jTSSKK7mYLJEQdUiUfykbG49rubC9SfDyvT2JrzTReopWlz2MxqeLyxh9UZhvxEIBgAhtrg==", "dev": true, "dependencies": { - "@babel/parser": "^7.24.4", - "@vue/shared": "3.4.27", + "@babel/parser": "^7.24.7", + "@vue/shared": "3.4.29", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-core/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/@vue/compiler-dom": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.27.tgz", - "integrity": "sha512-kUTvochG/oVgE1w5ViSr3KUBh9X7CWirebA3bezTbB5ZKBQZwR2Mwj9uoSKRMFcz4gSMzzLXBPD6KpCLb9nvWw==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.29.tgz", + "integrity": "sha512-A6+iZ2fKIEGnfPJejdB7b1FlJzgiD+Y/sxxKwJWg1EbJu6ZPgzaPQQ51ESGNv0CP6jm6Z7/pO6Ia8Ze6IKrX7w==", "dev": true, "dependencies": { - "@vue/compiler-core": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/compiler-core": "3.4.29", + "@vue/shared": "3.4.29" } }, "node_modules/@vue/compiler-dom/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/@vue/compiler-sfc": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.27.tgz", - "integrity": "sha512-nDwntUEADssW8e0rrmE0+OrONwmRlegDA1pD6QhVeXxjIytV03yDqTey9SBDiALsvAd5U4ZrEKbMyVXhX6mCGA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.29.tgz", + "integrity": "sha512-zygDcEtn8ZimDlrEQyLUovoWgKQic6aEQqRXce2WXBvSeHbEbcAsXyCk9oG33ZkyWH4sl9D3tkYc1idoOkdqZQ==", "dev": true, "dependencies": { - "@babel/parser": "^7.24.4", - "@vue/compiler-core": "3.4.27", - "@vue/compiler-dom": "3.4.27", - "@vue/compiler-ssr": "3.4.27", - "@vue/shared": "3.4.27", + "@babel/parser": "^7.24.7", + "@vue/compiler-core": "3.4.29", + "@vue/compiler-dom": "3.4.29", + "@vue/compiler-ssr": "3.4.29", + "@vue/shared": "3.4.29", "estree-walker": "^2.0.2", "magic-string": "^0.30.10", "postcss": "^8.4.38", @@ -587,25 +587,25 @@ } }, "node_modules/@vue/compiler-sfc/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/@vue/compiler-ssr": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.27.tgz", - "integrity": "sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.29.tgz", + "integrity": "sha512-rFbwCmxJ16tDp3N8XCx5xSQzjhidYjXllvEcqX/lopkoznlNPz3jyy0WGJCyhAaVQK677WWFt3YO/WUEkMMUFQ==", "dev": true, "dependencies": { - "@vue/compiler-dom": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/compiler-dom": "3.4.29", + "@vue/shared": "3.4.29" } }, "node_modules/@vue/compiler-ssr/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/@vue/reactivity": { @@ -617,64 +617,74 @@ } }, "node_modules/@vue/runtime-core": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.27.tgz", - "integrity": "sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.29.tgz", + "integrity": "sha512-s8fmX3YVR/Rk5ig0ic0NuzTNjK2M7iLuVSZyMmCzN/+Mjuqqif1JasCtEtmtoJWF32pAtUjyuT2ljNKNLeOmnQ==", "dev": true, "dependencies": { - "@vue/reactivity": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/reactivity": "3.4.29", + "@vue/shared": "3.4.29" } }, "node_modules/@vue/runtime-core/node_modules/@vue/reactivity": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.27.tgz", - "integrity": "sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.29.tgz", + "integrity": "sha512-w8+KV+mb1a8ornnGQitnMdLfE0kXmteaxLdccm2XwdFxXst4q/Z7SEboCV5SqJNpZbKFeaRBBJBhW24aJyGINg==", "dev": true, "dependencies": { - "@vue/shared": "3.4.27" + "@vue/shared": "3.4.29" } }, "node_modules/@vue/runtime-core/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/@vue/runtime-dom": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.27.tgz", - "integrity": "sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.29.tgz", + "integrity": "sha512-gI10atCrtOLf/2MPPMM+dpz3NGulo9ZZR9d1dWo4fYvm+xkfvRrw1ZmJ7mkWtiJVXSsdmPbcK1p5dZzOCKDN0g==", "dev": true, "dependencies": { - "@vue/runtime-core": "3.4.27", - "@vue/shared": "3.4.27", + "@vue/reactivity": "3.4.29", + "@vue/runtime-core": "3.4.29", + "@vue/shared": "3.4.29", "csstype": "^3.1.3" } }, + "node_modules/@vue/runtime-dom/node_modules/@vue/reactivity": { + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.29.tgz", + "integrity": "sha512-w8+KV+mb1a8ornnGQitnMdLfE0kXmteaxLdccm2XwdFxXst4q/Z7SEboCV5SqJNpZbKFeaRBBJBhW24aJyGINg==", + "dev": true, + "dependencies": { + "@vue/shared": "3.4.29" + } + }, "node_modules/@vue/runtime-dom/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/@vue/server-renderer": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.27.tgz", - "integrity": "sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.29.tgz", + "integrity": "sha512-HMLCmPI2j/k8PVkSBysrA2RxcxC5DgBiCdj7n7H2QtR8bQQPqKAe8qoaxLcInzouBmzwJ+J0x20ygN/B5mYBng==", "dev": true, "dependencies": { - "@vue/compiler-ssr": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/compiler-ssr": "3.4.29", + "@vue/shared": "3.4.29" }, "peerDependencies": { - "vue": "3.4.27" + "vue": "3.4.29" } }, "node_modules/@vue/server-renderer/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/@vue/shared": { @@ -789,11 +799,11 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1094,9 +1104,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1897,9 +1907,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", - "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", + "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -2082,16 +2092,16 @@ } }, "node_modules/vue": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.27.tgz", - "integrity": "sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.29.tgz", + "integrity": "sha512-8QUYfRcYzNlYuzKPfge1UWC6nF9ym0lx7mpGVPJYNhddxEf3DD0+kU07NTL0sXuiT2HuJuKr/iEO8WvXvT0RSQ==", "dev": true, "dependencies": { - "@vue/compiler-dom": "3.4.27", - "@vue/compiler-sfc": "3.4.27", - "@vue/runtime-dom": "3.4.27", - "@vue/server-renderer": "3.4.27", - "@vue/shared": "3.4.27" + "@vue/compiler-dom": "3.4.29", + "@vue/compiler-sfc": "3.4.29", + "@vue/runtime-dom": "3.4.29", + "@vue/server-renderer": "3.4.29", + "@vue/shared": "3.4.29" }, "peerDependencies": { "typescript": "*" @@ -2103,9 +2113,9 @@ } }, "node_modules/vue/node_modules/@vue/shared": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", - "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==", + "version": "3.4.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz", + "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==", "dev": true }, "node_modules/wrappy": { diff --git a/package.json b/package.json index 4d6b321c8..b4609a025 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ "laravel-vite-plugin": "0.8.1", "postcss": "8.4.38", "pusher-js": "8.4.0-rc2", - "tailwindcss": "3.4.3", + "tailwindcss": "3.4.4", "vite": "4.5.3", - "vue": "3.4.27" + "vue": "3.4.29" }, "dependencies": { "@tailwindcss/forms": "0.5.7", diff --git a/resources/css/app.css b/resources/css/app.css index 42496cffe..a037def51 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -4,7 +4,6 @@ html, body { - zoom: 0.95; @apply h-full bg-neutral-50 text-neutral-800 dark:bg-base dark:text-neutral-400; } @@ -12,6 +11,18 @@ body { @apply text-sm antialiased scrollbar; } +.apexcharts-tooltip { + @apply dark:text-white dark:border-coolgray-300 dark:bg-coolgray-200 shadow-none !important; +} + +.apexcharts-tooltip-title { + @apply hidden !important; +} + +.apexcharts-xaxistooltip { + @apply hidden !important; +} + .input, .select { @apply text-black dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300; @@ -116,15 +127,19 @@ tr td:first-child { .alert-error { @apply flex items-center gap-2 text-error; } + .tag { @apply px-2 py-1 cursor-pointer box-description dark:bg-coolgray-100 dark:hover:bg-coolgray-300 bg-neutral-100 hover:bg-neutral-200; } + .add-tag { @apply flex items-center px-2 text-xs cursor-pointer dark:text-neutral-500/20 text-neutral-500 group-hover:text-neutral-700 group-hover:dark:text-white dark:hover:bg-coolgray-300 hover:bg-neutral-200; } + .dropdown-item { @apply relative flex cursor-pointer select-none dark:text-white hover:bg-neutral-100 dark:hover:bg-coollabs items-center pr-4 pl-2 py-1 text-xs justify-start outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 gap-2 w-full; } + .dropdown-item-no-padding { @apply relative flex cursor-pointer select-none dark:text-white hover:bg-neutral-100 dark:hover:bg-coollabs items-center py-1 text-xs justify-start outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 gap-2 w-full; } @@ -200,12 +215,15 @@ tr td:first-child { .box { @apply relative flex lg:flex-row flex-col p-2 transition-colors cursor-pointer min-h-[4rem] dark:bg-coolgray-100 bg-white border text-black dark:text-white hover:text-black border-neutral-200 dark:border-black hover:bg-neutral-100 dark:hover:bg-coollabs-100 dark:hover:text-white hover:no-underline; } + .box-boarding { @apply flex lg:flex-row flex-col p-2 transition-colors cursor-pointer min-h-[4rem] dark:bg-coolgray-100 dark:text-white bg-neutral-50 border border-neutral-200 dark:border-black hover:bg-neutral-100 dark:hover:bg-coollabs-100 dark:hover:text-white hover:text-black hover:no-underline text-black; } + .box-without-bg { @apply flex p-2 transition-colors dark:hover:text-white hover:no-underline min-h-[4rem] border border-neutral-200 dark:border-black; } + .box-without-bg-without-border { @apply flex p-2 transition-colors dark:hover:text-white hover:no-underline min-h-[4rem]; } diff --git a/resources/views/components/forms/monaco-editor.blade.php b/resources/views/components/forms/monaco-editor.blade.php new file mode 100644 index 000000000..d3793785b --- /dev/null +++ b/resources/views/components/forms/monaco-editor.blade.php @@ -0,0 +1,437 @@ +
+
+
+
+
+
+
+
+
+
diff --git a/resources/views/components/forms/textarea.blade.php b/resources/views/components/forms/textarea.blade.php index 2c32c585e..24226ecdb 100644 --- a/resources/views/components/forms/textarea.blade.php +++ b/resources/views/components/forms/textarea.blade.php @@ -24,47 +24,54 @@ @endif @endif - @if ($type === 'password') -
- @if ($allowToPeak) -
- - - - - -
- @endif - merge(['class' => $defaultClassInput]) }} @required($required) - @if ($id !== 'null') wire:model={{ $id }} @endif - wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300' - wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" - type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $id }}" - name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}" - aria-placeholder="{{ $attributes->get('placeholder') }}"> - + @disabled($disabled) @readonly($readonly) @required($required) id="{{ $id }}" + name="{{ $name }}" name={{ $id }}> -
- @else - + @disabled($disabled) @readonly($readonly) @required($required) id="{{ $id }}" + name="{{ $name }}" name={{ $id }}> + @endif @endif - @error($id)