Merge branch 'next' of github.com:coollabsio/coolify into next

This commit is contained in:
Andras Bacsai 2024-06-24 09:57:42 +02:00
commit 758fab9976
39 changed files with 801 additions and 531 deletions

View File

@ -1,6 +1,10 @@
![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) [![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) [![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) [![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 # About the Project
Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc. Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc.

View File

@ -5,6 +5,7 @@
use App\Enums\ApplicationDeploymentStatus; use App\Enums\ApplicationDeploymentStatus;
use App\Jobs\CleanupHelperContainersJob; use App\Jobs\CleanupHelperContainersJob;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\Environment;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\Server; use App\Models\Server;
@ -24,6 +25,8 @@ public function handle()
get_public_ips(); get_public_ips();
$full_cleanup = $this->option('full-cleanup'); $full_cleanup = $this->option('full-cleanup');
$cleanup_deployments = $this->option('cleanup-deployments'); $cleanup_deployments = $this->option('cleanup-deployments');
$this->replace_slash_in_environment_name();
if ($cleanup_deployments) { if ($cleanup_deployments) {
echo "Running cleanup deployments.\n"; echo "Running cleanup deployments.\n";
$this->cleanup_in_progress_application_deployments(); $this->cleanup_in_progress_application_deployments();
@ -150,4 +153,15 @@ private function cleanup_in_progress_application_deployments()
echo "Error: {$e->getMessage()}\n"; 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();
}
}
}
} }

View File

@ -0,0 +1,183 @@
<?php
namespace App\Http\Controllers\Api;
use App\Actions\Application\StopApplication;
use App\Http\Controllers\Controller;
use App\Models\Application;
use App\Models\Project;
use Illuminate\Http\Request;
use Visus\Cuid2\Cuid2;
class Applications extends Controller
{
public function applications(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$projects = Project::where('team_id', $teamId)->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
);
}
}

View File

@ -38,7 +38,25 @@ public function deployments(Request $request)
'status', 'status',
])->sortBy('id')->toArray(); ])->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) public function deploy(Request $request)

View File

@ -4,161 +4,11 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Application; use App\Models\Application;
use App\Models\InstanceSettings;
use App\Models\Project as ModelsProject;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
class Domains extends Controller class Domains extends Controller
{ {
public function domains(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 = 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();
return response()->json($domains);
}
public function updateDomains(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$validator = Validator::make($request->all(), [
'uuid' => 'required|string|exists:applications,uuid',
'domains' => 'required|array',
'domains.*' => 'required|string|distinct',
]);
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);
$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([
'success' => true,
'message' => 'Domains updated successfully',
'application' => $application,
]);
}
public function deleteDomains(Request $request) public function deleteDomains(Request $request)
{ {
$teamId = get_team_id_from_token(); $teamId = get_team_id_from_token();

View File

@ -3,6 +3,9 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Application;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\Server as ModelsServer; use App\Models\Server as ModelsServer;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -59,4 +62,106 @@ public function server_by_uuid(Request $request)
return response()->json($server); 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);
}
} }

View File

@ -2,8 +2,10 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\InstanceSettings;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpKernel\Exception\HttpException;
class OauthController extends Controller class OauthController extends Controller
{ {
@ -20,6 +22,11 @@ public function callback(string $provider)
$oauthUser = get_socialite_provider($provider)->user(); $oauthUser = get_socialite_provider($provider)->user();
$user = User::whereEmail($oauthUser->email)->first(); $user = User::whereEmail($oauthUser->email)->first();
if (! $user) { if (! $user) {
$settings = InstanceSettings::get();
if (! $settings->is_registration_enabled) {
abort(403, 'Registration is disabled');
}
$user = User::create([ $user = User::create([
'name' => $oauthUser->name, 'name' => $oauthUser->name,
'email' => $oauthUser->email, 'email' => $oauthUser->email,
@ -31,7 +38,9 @@ public function callback(string $provider)
} catch (\Exception $e) { } catch (\Exception $e) {
ray($e->getMessage()); 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)]);
} }
} }
} }

View File

@ -1,59 +0,0 @@
<?php
namespace App\Livewire\Charts;
use App\Models\Server as ModelsServer;
use Livewire\Component;
class ServerCpu extends Component
{
public ModelsServer $server;
public $chartId = 'server-cpu';
public $data;
public $categories;
public int $interval = 5;
public bool $poll = true;
public function render()
{
return view('livewire.charts.server-cpu');
}
public function pollData()
{
if ($this->poll || $this->interval <= 10) {
$this->loadData();
if ($this->interval > 10) {
$this->poll = false;
}
}
}
public function loadData()
{
try {
$metrics = $this->server->getCpuMetrics($this->interval);
$metrics = collect($metrics)->map(function ($metric) {
return [$metric[0], $metric[1]];
});
$this->dispatch("refreshChartData-{$this->chartId}", [
'seriesData' => $metrics,
]);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function setInterval()
{
if ($this->interval <= 10) {
$this->poll = true;
}
$this->loadData();
}
}

View File

@ -176,10 +176,12 @@ public function setType(string $type)
return; return;
} }
// if (count($this->servers) === 1) { if (count($this->servers) === 1) {
// $server = $this->servers->first(); $server = $this->servers->first();
// $this->setServer($server); if ($server instanceof Server) {
// } $this->setServer($server);
}
}
if (! is_null($this->server)) { if (! is_null($this->server)) {
$foundServer = $this->servers->where('id', $this->server->id)->first(); $foundServer = $this->servers->where('id', $this->server->id)->first();
if ($foundServer) { if ($foundServer) {
@ -195,6 +197,13 @@ public function setServer(Server $server)
$this->server = $server; $this->server = $server;
$this->standaloneDockers = $server->standaloneDockers; $this->standaloneDockers = $server->standaloneDockers;
$this->swarmDockers = $server->swarmDockers; $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'; $this->current_step = 'destinations';
} }

View File

@ -0,0 +1,35 @@
<?php
namespace App\Livewire\Project\Resource;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Component;
class EnvironmentSelect extends Component
{
public Collection $environments;
public string $project_uuid = '';
public string $selectedEnvironment = '';
public function mount()
{
$this->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,
]);
}
}
}

View File

@ -11,6 +11,8 @@ class EditCompose extends Component
public $serviceId; public $serviceId;
protected $listeners = ['refreshEnvs' => 'mount'];
protected $rules = [ protected $rules = [
'service.docker_compose_raw' => 'required', 'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required', 'service.docker_compose' => 'required',

View File

@ -75,7 +75,6 @@ public function submit()
$this->service->parse(); $this->service->parse();
$this->service->refresh(); $this->service->refresh();
$this->service->saveComposeConfigs(); $this->service->saveComposeConfigs();
$this->dispatch('refreshStacks');
$this->dispatch('refreshEnvs'); $this->dispatch('refreshEnvs');
$this->dispatch('success', 'Service saved.'); $this->dispatch('success', 'Service saved.');
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@ -1,15 +1,15 @@
<?php <?php
namespace App\Livewire\Charts; namespace App\Livewire\Server;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
class ServerMemory extends Component class Charts extends Component
{ {
public Server $server; public Server $server;
public $chartId = 'server-memory'; public $chartId = 'server';
public $data; public $data;
@ -19,11 +19,6 @@ class ServerMemory extends Component
public bool $poll = true; public bool $poll = true;
public function render()
{
return view('livewire.charts.server-memory');
}
public function pollData() public function pollData()
{ {
if ($this->poll || $this->interval <= 10) { if ($this->poll || $this->interval <= 10) {
@ -37,13 +32,21 @@ public function pollData()
public function loadData() public function loadData()
{ {
try { try {
$metrics = $this->server->getMemoryMetrics($this->interval); $cpuMetrics = $this->server->getCpuMetrics($this->interval);
$metrics = collect($metrics)->map(function ($metric) { $memoryMetrics = $this->server->getMemoryMetrics($this->interval);
$cpuMetrics = collect($cpuMetrics)->map(function ($metric) {
return [$metric[0], $metric[1]]; return [$metric[0], $metric[1]];
}); });
$this->dispatch("refreshChartData-{$this->chartId}", [ $memoryMetrics = collect($memoryMetrics)->map(function ($metric) {
'seriesData' => $metrics, return [$metric[0], $metric[1]];
});
$this->dispatch("refreshChartData-{$this->chartId}-cpu", [
'seriesData' => $cpuMetrics,
]); ]);
$this->dispatch("refreshChartData-{$this->chartId}-memory", [
'seriesData' => $memoryMetrics,
]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@ -228,7 +228,7 @@ public function gitCommits(): Attribute
public function gitCommitLink($link): string 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')) { 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}/commits/{$link}";
} }
@ -245,8 +245,11 @@ public function gitCommitLink($link): string
} }
if (strpos($this->git_repository, 'git@') === 0) { if (strpos($this->git_repository, 'git@') === 0) {
$git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository); $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 "https://{$git_repository}/commit/{$link}"; return "{$git_repository}/commit/{$link}";
} }
return $this->git_repository; return $this->git_repository;

View File

@ -109,7 +109,7 @@ public function services()
protected function name(): Attribute protected function name(): Attribute
{ {
return Attribute::make( return Attribute::make(
set: fn (string $value) => strtolower($value), set: fn (string $value) => str($value)->lower()->trim()->replace('/', '-')->toString(),
); );
} }
} }

View File

@ -112,4 +112,14 @@ public function databases()
{ {
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()); 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;
}
} }

View File

@ -839,20 +839,33 @@ public function saveComposeConfigs()
$commands[] = "cd $workdir"; $commands[] = "cd $workdir";
$json = Yaml::parse($this->docker_compose); $json = Yaml::parse($this->docker_compose);
$envs_from_coolify = $this->environment_variables()->get();
foreach ($json['services'] as $service => $config) { foreach ($json['services'] as $service => $config) {
$envs = collect($config['environment']); $envs = collect($config['environment']);
$envs->push("COOLIFY_CONTAINER_NAME=$service-{$this->uuid}"); $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()); data_set($json, "services.$service.environment", $envs->toArray());
} }
$this->docker_compose = Yaml::dump($json);
$this->docker_compose = Yaml::dump($json, 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
$docker_compose_base64 = base64_encode($this->docker_compose); $docker_compose_base64 = base64_encode($this->docker_compose);
$commands[] = "echo $docker_compose_base64 | base64 -d | tee docker-compose.yml > /dev/null"; $commands[] = "echo $docker_compose_base64 | base64 -d | tee docker-compose.yml > /dev/null";
$envs = $this->environment_variables()->get();
$commands[] = 'rm -f .env || true'; $commands[] = 'rm -f .env || true';
foreach ($envs as $env) {
foreach ($envs_from_coolify as $env) {
$commands[] = "echo '{$env->key}={$env->real_value}' >> .env"; $commands[] = "echo '{$env->key}={$env->real_value}' >> .env";
} }
if ($envs->count() === 0) { if ($envs_from_coolify->count() === 0) {
$commands[] = 'touch .env'; $commands[] = 'touch .env';
} }
instant_remote_process($commands, $this->server); instant_remote_process($commands, $this->server);

View File

@ -1,5 +1,7 @@
<?php <?php
use Illuminate\Database\Eloquent\Collection;
function get_team_id_from_token() function get_team_id_from_token()
{ {
$token = auth()->user()->currentAccessToken(); $token = auth()->user()->currentAccessToken();
@ -10,3 +12,27 @@ function invalid_token()
{ {
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api-reference/authorization'], 400); 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;
}

View File

@ -8,7 +8,7 @@
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use Spatie\Url\Url; 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; $application_id = $application->id;
$deployment_link = Url::fromString($application->link()."/deployment/{$deployment_uuid}"); $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, 'pull_request_id' => $pull_request_id,
'force_rebuild' => $force_rebuild, 'force_rebuild' => $force_rebuild,
'is_webhook' => $is_webhook, 'is_webhook' => $is_webhook,
'is_api' => $is_api,
'restart_only' => $restart_only, 'restart_only' => $restart_only,
'commit' => $commit, 'commit' => $commit,
'rollback' => $rollback, 'rollback' => $rollback,

View File

@ -1254,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 // Add labels to the service
if ($savedService->serviceType()) { if ($savedService->serviceType()) {
@ -1318,19 +1330,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
data_forget($service, 'volumes.*.isDirectory'); data_forget($service, 'volumes.*.isDirectory');
data_forget($service, 'volumes.*.is_directory'); data_forget($service, 'volumes.*.is_directory');
data_forget($service, 'exclude_from_hc'); data_forget($service, 'exclude_from_hc');
data_set($service, 'environment', $serviceVariables->toArray());
// 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());
updateCompose($savedService); updateCompose($savedService);
return $service; return $service;

View File

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

View File

@ -1,116 +0,0 @@
<div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()">
<h3>CPU (%)</h3>
<x-forms.select label="Interval" wire:change="setInterval" id="interval">
<option value="5">5 minutes (live)</option>
<option value="10">10 minutes (live)</option>
<option value="30">30 minutes</option>
<option value="60">1 hour</option>
<option value="720">12 hours</option>
<option value="10080">1 week</option>
<option value="43200">30 days</option>
</x-forms.select>
<div wire:ignore id="{!! $chartId !!}"></div>
<script>
checkTheme();
const optionsServerCpu = {
stroke: {
curve: 'straight',
},
chart: {
height: '150px',
id: '{!! $chartId !!}',
type: 'area',
toolbar: {
show: false,
tools: {
download: true,
selection: false,
zoom: false,
zoomin: false,
zoomout: false,
pan: false,
reset: false
},
},
animations: {
enabled: false,
},
},
fill: {
type: 'gradient',
},
dataLabels: {
enabled: false,
offsetY: -10,
style: {
colors: ['#FCD452'],
},
background: {
enabled: false,
}
},
grid: {
show: true,
borderColor: '',
},
colors: [baseColor],
xaxis: {
type: 'datetime',
},
series: [{
data: []
}],
noData: {
text: 'Loading...',
style: {
color: textColor,
}
},
tooltip: {
enabled: false,
},
legend: {
show: false
}
}
const serverCpuChart = new ApexCharts(document.getElementById(`{!! $chartId !!}`), optionsServerCpu);
serverCpuChart.render();
document.addEventListener('livewire:init', () => {
Livewire.on('refreshChartData-{!! $chartId !!}', (chartData) => {
checkTheme();
serverCpuChart.updateOptions({
series: [{
data: chartData[0].seriesData,
}],
colors: [baseColor],
xaxis: {
type: 'datetime',
labels: {
show: true,
style: {
colors: textColor,
}
}
},
yaxis: {
show: true,
labels: {
show: true,
style: {
colors: textColor,
}
}
},
noData: {
text: 'Loading...',
style: {
color: textColor,
}
}
});
});
});
</script>
</div>

View File

@ -1,123 +0,0 @@
<div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()">
<h3>Memory (MB)</h3>
<x-forms.select label="Interval" wire:change="setInterval" id="interval">
<option value="5">5 minutes (live)</option>
<option value="10">10 minutes (live)</option>
<option value="30">30 minutes</option>
<option value="60">1 hour</option>
<option value="720">12 hours</option>
<option value="10080">1 week</option>
<option value="43200">30 days</option>
</x-forms.select>
<div wire:ignore id="{!! $chartId !!}"></div>
<script>
checkTheme();
const optionsServerMemory = {
stroke: {
curve: 'straight',
},
chart: {
height: '150px',
id: '{!! $chartId !!}',
type: 'area',
toolbar: {
show: false,
tools: {
download: true,
selection: false,
zoom: false,
zoomin: false,
zoomout: false,
pan: false,
reset: false
},
},
animations: {
enabled: false,
},
},
fill: {
type: 'gradient',
},
dataLabels: {
enabled: false,
offsetY: -10,
style: {
colors: ['#FCD452'],
},
background: {
enabled: false,
}
},
grid: {
show: true,
borderColor: '',
},
colors: [baseColor],
xaxis: {
type: 'datetime',
labels: {
show: true,
style: {
colors: textColor,
}
}
},
series: [{
data: []
}],
noData: {
text: 'Loading...',
style: {
color: textColor,
}
},
tooltip: {
enabled: false,
},
legend: {
show: false
}
}
const serverMemoryChart = new ApexCharts(document.getElementById(`{!! $chartId !!}`), optionsServerMemory);
serverMemoryChart.render();
document.addEventListener('livewire:init', () => {
Livewire.on('refreshChartData-{!! $chartId !!}', (chartData) => {
checkTheme();
serverMemoryChart.updateOptions({
series: [{
data: chartData[0].seriesData,
}],
colors: [baseColor],
xaxis: {
type: 'datetime',
labels: {
show: true,
style: {
colors: textColor,
}
}
},
yaxis: {
min: 0,
show: true,
labels: {
show: true,
style: {
colors: textColor,
}
}
},
noData: {
text: 'Loading...',
style: {
color: textColor,
}
}
});
});
});
</script>
</div>

View File

@ -23,9 +23,7 @@
<div class="grid grid-cols-1 gap-2 xl:grid-cols-2"> <div class="grid grid-cols-1 gap-2 xl:grid-cols-2">
@foreach ($projects as $project) @foreach ($projects as $project)
<div class="gap-2 border border-transparent cursor-pointer box group" <div class="gap-2 border border-transparent cursor-pointer box group"
@if (data_get($project, 'environments')->count() === 1) onclick="gotoProject('{{ data_get($project, 'uuid') }}', '{{ data_get($project, 'environments.0.name', 'production') }}')" onclick="window.location.href = '{{ route('project.resource.index', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => $project->default_environment()->name]) }}'">
@else
onclick="window.location.href = '{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}'" @endif>
<div class="flex flex-1 mx-6"> <div class="flex flex-1 mx-6">
<div class="flex flex-col justify-center flex-1"> <div class="flex flex-col justify-center flex-1">
<div class="box-title">{{ $project->name }}</div> <div class="box-title">{{ $project->name }}</div>

View File

@ -40,7 +40,7 @@
<x-forms.button type="submit">Validate 2FA</x-forms.button> <x-forms.button type="submit">Validate 2FA</x-forms.button>
</form> </form>
<div> <div>
<div>{!! request()->user()->twoFactorQrCodeSvg() !!}</div> <div class="flex items-center justify-center w-64 h-64 bg-transparent">{!! request()->user()->twoFactorQrCodeSvg() !!}</div>
<div x-data="{ showCode: false }" class="py-2"> <div x-data="{ showCode: false }" class="py-2">
<template x-if="showCode"> <template x-if="showCode">
<div class="py-2 ">{!! decrypt(request()->user()->two_factor_secret) !!}</div> <div class="py-2 ">{!! decrypt(request()->user()->two_factor_secret) !!}</div>

View File

@ -75,7 +75,11 @@ class="w-6 h-6" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@if (data_get($deployment, 'rollback') === true) @if (data_get($deployment, 'rollback') === true)
Rollback Rollback
@else @else
Manual @if (data_get($deployment, 'is_api'))
API
@else
Manual
@endif
@endif @endif
@if (data_get($deployment, 'commit')) @if (data_get($deployment, 'commit'))
<div class="dark:hover:text-white" <div class="dark:hover:text-white"

View File

@ -27,12 +27,6 @@
<div class="flex items-center"> <div class="flex items-center">
<a class="text-xs truncate lg:text-sm" <a class="text-xs truncate lg:text-sm"
href="{{ route('project.resource.index', ['environment_name' => data_get($parameters, 'environment_name'), 'project_uuid' => data_get($parameters, 'project_uuid')]) }}">{{ data_get($parameters, 'environment_name') }}</a> href="{{ route('project.resource.index', ['environment_name' => data_get($parameters, 'environment_name'), 'project_uuid' => data_get($parameters, 'project_uuid')]) }}">{{ data_get($parameters, 'environment_name') }}</a>
<svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold dark:text-warning" fill="currentColor"
viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path>
</svg>
</div> </div>
</li> </li>
<li> <li>

View File

@ -13,7 +13,7 @@
@forelse ($projects as $project) @forelse ($projects as $project)
<div class="box group" x-data x-on:click="goto('{{ $project->uuid }}')"> <div class="box group" x-data x-on:click="goto('{{ $project->uuid }}')">
<a class="flex flex-col justify-center flex-1 mx-6" <a class="flex flex-col justify-center flex-1 mx-6"
href="{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}"> href="{{ route('project.resource.index', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => $project->default_environment()->name]) }}">
<div class="box-title">{{ $project->name }}</div> <div class="box-title">{{ $project->name }}</div>
<div class="box-description "> <div class="box-description ">
{{ $project->description }}</div> {{ $project->description }}</div>

View File

@ -2,7 +2,7 @@
<div class="flex flex-col gap-4 lg:flex-row "> <div class="flex flex-col gap-4 lg:flex-row ">
<h1>New Resource</h1> <h1>New Resource</h1>
<div class="w-full pb-4 lg:w-96 lg:pb-0"> <div class="w-full pb-4 lg:w-96 lg:pb-0">
<x-forms.select wire:model="selectedEnvironment"> <x-forms.select wire:model.live="selectedEnvironment">
@foreach ($environments as $environment) @foreach ($environments as $environment)
<option value="{{ $environment->name }}">Environment: {{ $environment->name }}</option> <option value="{{ $environment->name }}">Environment: {{ $environment->name }}</option>
@endforeach @endforeach
@ -533,7 +533,6 @@ class="w-[4.5rem] aspect-square h-[4.5rem] p-2 transition-all duration-200 opaci
@if (file_exists(public_path(data_get($service, 'logo')))) @if (file_exists(public_path(data_get($service, 'logo'))))
<img class="w-[4.5rem] aspect-square h-[4.5rem] p-2 transition-all duration-200 opacity-30 grayscale group-hover:grayscale-0 group-hover:opacity-100 bg-neutral-300 dark:bg-transparent hover:bg-neutral-800" <img class="w-[4.5rem] aspect-square h-[4.5rem] p-2 transition-all duration-200 opacity-30 grayscale group-hover:grayscale-0 group-hover:opacity-100 bg-neutral-300 dark:bg-transparent hover:bg-neutral-800"
src="{{ asset(data_get($service, 'logo')) }}"> src="{{ asset(data_get($service, 'logo')) }}">
@endif @endif
</x-slot:logo> </x-slot:logo>
<x-slot:documentation> <x-slot:documentation>

View File

@ -0,0 +1,8 @@
<x-forms.select wire:model.live="selectedEnvironment">
<option value="edit">Create/Edit Environments</option>
<option disabled>-----</option>
@foreach ($environments as $environment)
<option value="{{ $environment->name }}">{{ $environment->name }}
</option>
@endforeach
</x-forms.select>

View File

@ -21,7 +21,7 @@ class="button">+
@endif @endif
<livewire:project.delete-environment :disabled="!$environment->isEmpty()" :environment_id="$environment->id" /> <livewire:project.delete-environment :disabled="!$environment->isEmpty()" :environment_id="$environment->id" />
</div> </div>
<nav class="flex pt-2 pb-10"> <nav class="flex pt-2 pb-6">
<ol class="flex items-center"> <ol class="flex items-center">
<li class="inline-flex items-center"> <li class="inline-flex items-center">
<a class="text-xs truncate lg:text-sm" <a class="text-xs truncate lg:text-sm"
@ -36,15 +36,18 @@ class="button">+
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path> clip-rule="evenodd"></path>
</svg> </svg>
<a class="text-xs truncate lg:text-sm"
href="{{ route('project.resource.index', ['environment_name' => request()->route('environment_name'), 'project_uuid' => request()->route('project_uuid')]) }}">{{ request()->route('environment_name') }}</a> <livewire:project.resource.environment-select :environments="$project->environments" />
</div> </div>
</li> </li>
<li>
</li>
</ol> </ol>
</nav> </nav>
</div> </div>
@if ($environment->isEmpty()) @if ($environment->isEmpty())
<a href="{{ route('project.resource.create', ['project_uuid' => request()->route('project_uuid'), 'environment_name' => request()->route('environment_name')]) }} " <a href="{{ route('project.resource.create', ['project_uuid' => request()->route('project_uuid'), 'environment_name' => request()->route('environment_name')]) }} "
class="items-center justify-center box">+ Add New Resource</a> class="items-center justify-center box">+ Add New Resource</a>
@else @else
<div x-data="searchComponent()"> <div x-data="searchComponent()">

View File

@ -2,13 +2,11 @@
<div> <div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h2>Environment Variables</h2> <h2>Environment Variables</h2>
@if ($resource->type() !== 'service') <x-modal-input buttonTitle="+ Add" title="New Environment Variable">
<x-modal-input buttonTitle="+ Add" title="New Environment Variable"> <livewire:project.shared.environment-variable.add />
<livewire:project.shared.environment-variable.add /> </x-modal-input>
</x-modal-input> <x-forms.button
<x-forms.button wire:click='switch'>{{ $view === 'normal' ? 'Developer view' : 'Normal view' }}</x-forms.button>
wire:click='switch'>{{ $view === 'normal' ? 'Developer view' : 'Normal view' }}</x-forms.button>
@endif
</div> </div>
<div>Environment variables (secrets) for this resource.</div> <div>Environment variables (secrets) for this resource.</div>
@if ($this->resourceClass === 'App\Models\Application' && data_get($this->resource, 'build_pack') !== 'dockercompose') @if ($this->resourceClass === 'App\Models\Application' && data_get($this->resource, 'build_pack') !== 'dockercompose')
@ -19,8 +17,17 @@
</div> </div>
@endif @endif
@if ($resource->type() === 'service' || $resource?->build_pack === 'dockercompose') @if ($resource->type() === 'service' || $resource?->build_pack === 'dockercompose')
<div class="pt-4 dark:text-warning text-coollabs">Hardcoded variables are not shown here.</div> <div class="flex items-center gap-1 pt-4 dark:text-warning text-coollabs">
<div class="pb-4 dark:text-warning text-coollabs">If you would like to add a variable, you must add it to your compose file (General tab).</div> <svg class="hidden w-4 h-4 dark:text-warning lg:block" viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16">
</path>
</svg>
Hardcoded variables are not shown here.
</div>
{{-- <div class="pb-4 dark:text-warning text-coollabs">If you would like to add a variable, you must add it to
your compose file.</div> --}}
@endif @endif
</div> </div>
@if ($view === 'normal') @if ($view === 'normal')

View File

@ -1,7 +1,7 @@
<div> <div>
<form wire:submit='submit' <form wire:submit='submit'
class="flex flex-col items-center gap-4 p-4 bg-white border lg:items-start dark:bg-base dark:border-coolgray-300"> class="flex flex-col items-center gap-4 p-4 bg-white border lg:items-start dark:bg-base dark:border-coolgray-300">
@if (!$env->isFoundInCompose && !$isSharedVariable) {{-- @if (!$env->isFoundInCompose && !$isSharedVariable)
<div class="flex items-center justify-center gap-2 dark:text-warning text-coollabs"> <svg <div class="flex items-center justify-center gap-2 dark:text-warning text-coollabs"> <svg
class="hidden w-4 h-4 dark:text-warning lg:block" viewBox="0 0 256 256" class="hidden w-4 h-4 dark:text-warning lg:block" viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
@ -9,7 +9,7 @@ class="hidden w-4 h-4 dark:text-warning lg:block" viewBox="0 0 256 256"
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16"> d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16">
</path> </path>
</svg>This variable is not found in the compose file, so it won't be used.</div> </svg>This variable is not found in the compose file, so it won't be used.</div>
@endif @endif --}}
@if ($isLocked) @if ($isLocked)
<div class="flex flex-1 w-full gap-2"> <div class="flex flex-1 w-full gap-2">
<x-forms.input disabled id="env.key" /> <x-forms.input disabled id="env.key" />

View File

@ -0,0 +1,232 @@
<div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()">
<h3>CPU (%)</h3>
<x-forms.select label="Interval" wire:change="setInterval" id="interval">
<option value="5">5 minutes (live)</option>
<option value="10">10 minutes (live)</option>
<option value="30">30 minutes</option>
<option value="60">1 hour</option>
<option value="720">12 hours</option>
<option value="10080">1 week</option>
<option value="43200">30 days</option>
</x-forms.select>
<div wire:ignore id="{!! $chartId !!}-cpu"></div>
<script>
checkTheme();
const optionsServerCpu = {
stroke: {
curve: 'straight',
},
chart: {
height: '150px',
id: '{!! $chartId !!}-cpu',
type: 'area',
toolbar: {
show: false,
tools: {
download: true,
selection: false,
zoom: false,
zoomin: false,
zoomout: false,
pan: false,
reset: false
},
},
animations: {
enabled: false,
},
},
fill: {
type: 'gradient',
},
dataLabels: {
enabled: false,
offsetY: -10,
style: {
colors: ['#FCD452'],
},
background: {
enabled: false,
}
},
grid: {
show: true,
borderColor: '',
},
colors: [baseColor],
xaxis: {
type: 'datetime',
},
series: [{
data: []
}],
noData: {
text: 'Loading...',
style: {
color: textColor,
}
},
tooltip: {
enabled: false,
},
legend: {
show: false
}
}
const serverCpuChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-cpu`),
optionsServerCpu);
serverCpuChart.render();
document.addEventListener('livewire:init', () => {
Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => {
checkTheme();
serverCpuChart.updateOptions({
series: [{
data: chartData[0].seriesData,
}],
colors: [baseColor],
xaxis: {
type: 'datetime',
labels: {
show: true,
style: {
colors: textColor,
}
}
},
yaxis: {
show: true,
labels: {
show: true,
style: {
colors: textColor,
}
}
},
noData: {
text: 'Loading...',
style: {
color: textColor,
}
}
});
});
});
</script>
<div>
<h3>Memory (MB)</h3>
<div wire:ignore id="{!! $chartId !!}-memory"></div>
<script>
checkTheme();
const optionsServerMemory = {
stroke: {
curve: 'straight',
},
chart: {
height: '150px',
id: '{!! $chartId !!}-memory',
type: 'area',
toolbar: {
show: false,
tools: {
download: true,
selection: false,
zoom: false,
zoomin: false,
zoomout: false,
pan: false,
reset: false
},
},
animations: {
enabled: false,
},
},
fill: {
type: 'gradient',
},
dataLabels: {
enabled: false,
offsetY: -10,
style: {
colors: ['#FCD452'],
},
background: {
enabled: false,
}
},
grid: {
show: true,
borderColor: '',
},
colors: [baseColor],
xaxis: {
type: 'datetime',
labels: {
show: true,
style: {
colors: textColor,
}
}
},
series: [{
data: []
}],
noData: {
text: 'Loading...',
style: {
color: textColor,
}
},
tooltip: {
enabled: false,
},
legend: {
show: false
}
}
const serverMemoryChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-memory`),
optionsServerMemory);
serverMemoryChart.render();
document.addEventListener('livewire:init', () => {
Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => {
checkTheme();
serverMemoryChart.updateOptions({
series: [{
data: chartData[0].seriesData,
}],
colors: [baseColor],
xaxis: {
type: 'datetime',
labels: {
show: true,
style: {
colors: textColor,
}
}
},
yaxis: {
min: 0,
show: true,
labels: {
show: true,
style: {
colors: textColor,
}
}
},
noData: {
text: 'Loading...',
style: {
color: textColor,
}
}
});
});
});
</script>
</div>
</div>

View File

@ -7,8 +7,7 @@
<livewire:server.delete :server="$server" /> <livewire:server.delete :server="$server" />
@if ($server->isFunctional() && $server->isMetricsEnabled()) @if ($server->isFunctional() && $server->isMetricsEnabled())
<div class="pt-10"> <div class="pt-10">
<livewire:charts.server-cpu :server="$server" /> <livewire:server.charts :server="$server" />
<livewire:charts.server-memory :server="$server" />
</div> </div>
@endif @endif
</div> </div>

View File

@ -1,5 +1,6 @@
<?php <?php
use App\Http\Controllers\Api\Applications;
use App\Http\Controllers\Api\Deploy; use App\Http\Controllers\Api\Deploy;
use App\Http\Controllers\Api\Domains; use App\Http\Controllers\Api\Domains;
use App\Http\Controllers\Api\Resources; use App\Http\Controllers\Api\Resources;
@ -31,16 +32,23 @@
Route::get('/version', function () { Route::get('/version', function () {
return response(config('version')); return response(config('version'));
}); });
Route::get('/deploy', [Deploy::class, 'deploy']); Route::match(['get', 'post'], '/deploy', [Deploy::class, 'deploy']);
Route::get('/deployments', [Deploy::class, 'deployments']); Route::get('/deployments', [Deploy::class, 'deployments']);
Route::get('/deployment/{uuid}', [Deploy::class, 'deployment_by_uuid']);
Route::get('/servers', [Server::class, 'servers']); Route::get('/servers', [Server::class, 'servers']);
Route::get('/server/{uuid}', [Server::class, 'server_by_uuid']); Route::get('/server/{uuid}', [Server::class, 'server_by_uuid']);
Route::get('/servers/domains', [Server::class, 'get_domains_by_server']);
Route::get('/resources', [Resources::class, 'resources']); Route::get('/resources', [Resources::class, 'resources']);
Route::get('/domains', [Domains::class, 'domains']); Route::get('/applications', [Applications::class, 'applications']);
Route::put('/domains', [Domains::class, 'updateDomains']); Route::get('/application/{uuid}', [Applications::class, 'application_by_uuid']);
Route::put('/application/{uuid}', [Applications::class, 'update_by_uuid']);
Route::match(['get', 'post'], '/application/{uuid}/action/deploy', [Applications::class, 'action_deploy']);
Route::match(['get', 'post'], '/application/{uuid}/action/restart', [Applications::class, 'action_restart']);
Route::match(['get', 'post'], '/application/{uuid}/action/stop', [Applications::class, 'action_stop']);
Route::delete('/domains', [Domains::class, 'deleteDomains']); Route::delete('/domains', [Domains::class, 'deleteDomains']);
Route::get('/teams', [Team::class, 'teams']); Route::get('/teams', [Team::class, 'teams']);
@ -49,12 +57,12 @@
Route::get('/team/{id}', [Team::class, 'team_by_id']); Route::get('/team/{id}', [Team::class, 'team_by_id']);
Route::get('/team/{id}/members', [Team::class, 'members_by_id']); Route::get('/team/{id}/members', [Team::class, 'members_by_id']);
//Route::get('/projects', [Project::class, 'projects']); // Route::get('/projects', [Project::class, 'projects']);
//Route::get('/project/{uuid}', [Project::class, 'project_by_uuid']); //Route::get('/project/{uuid}', [Project::class, 'project_by_uuid']);
//Route::get('/project/{uuid}/{environment_name}', [Project::class, 'environment_details']); //Route::get('/project/{uuid}/{environment_name}', [Project::class, 'environment_details']);
}); });
Route::get('/{any}', function () { Route::any('/{any}', function () {
return response()->json(['error' => 'Not found.'], 404); return response()->json(['error' => 'Not found.'], 404);
})->where('any', '.*'); })->where('any', '.*');

View File

@ -9,7 +9,7 @@ services:
image: docker.io/library/postgres:12-alpine image: docker.io/library/postgres:12-alpine
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -d authentik -U $${SERVICE_USER_POSTGRESQL}"] test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
interval: 2s interval: 2s
timeout: 10s timeout: 10s
retries: 15 retries: 15
@ -55,8 +55,10 @@ services:
- ./media:/media - ./media:/media
- ./custom-templates:/templates - ./custom-templates:/templates
depends_on: depends_on:
- postgresql postgresql:
- redis condition: service_healthy
redis:
condition: service_healthy
authentik-worker: authentik-worker:
image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG:-2024.2.2} image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG:-2024.2.2}
restart: unless-stopped restart: unless-stopped
@ -90,5 +92,7 @@ services:
- ./certs:/certs - ./certs:/certs
- ./custom-templates:/templates - ./custom-templates:/templates
depends_on: depends_on:
- postgresql postgresql:
- redis condition: service_healthy
redis:
condition: service_healthy

View File

@ -1012,7 +1012,7 @@ services:
"-o", "-o",
"/dev/null", "/dev/null",
"-H", "-H",
"Authorization: Bearer ${ANON_KEY}", "Authorization: Bearer ${SERVICE_SUPABASEANON_KEY}",
"http://127.0.0.1:4000/api/tenants/realtime-dev/health" "http://127.0.0.1:4000/api/tenants/realtime-dev/health"
] ]
timeout: 5s timeout: 5s

File diff suppressed because one or more lines are too long