puh, fixes

This commit is contained in:
Andras Bacsai 2023-09-26 14:45:52 +02:00
parent 03c9793d11
commit fabb97330a
32 changed files with 386 additions and 127 deletions

View File

@ -12,6 +12,7 @@ public function handle(Service $service)
{
$service->saveComposeConfigs();
$commands[] = "cd " . $service->workdir();
$commands[] = "echo '####### Saved configuration files to {$service->workdir()}.'";
$commands[] = "echo '####### Starting service {$service->name} on {$service->server->name}.'";
$commands[] = "echo '####### Pulling images.'";
$commands[] = "docker compose pull";

View File

@ -69,7 +69,12 @@ public function new()
if ($type->startsWith('one-click-service-')) {
$oneClickServiceName = $type->after('one-click-service-')->value();
$oneClickService = data_get($services, "$oneClickServiceName.compose");
$oneClickDotEnvs = collect(data_get($services, "$oneClickServiceName.envs", []));
$oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null);
$oneClickRequiredFqdn = data_get($services, "$oneClickServiceName.generateFqdn", []);
$oneClickRequiredFqdn = collect($oneClickRequiredFqdn);
if ($oneClickDotEnvs) {
$oneClickDotEnvs = Str::of(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/');
}
if ($oneClickService) {
$service = Service::create([
'name' => "$oneClickServiceName-" . Str::random(10),
@ -77,18 +82,37 @@ public function new()
'environment_id' => $environment->id,
'server_id' => (int) $server_id,
]);
if ($oneClickDotEnvs->count() > 0) {
$oneClickDotEnvs->each(function ($value, $key) use ($service) {
$service->name = "$oneClickServiceName-" . $service->uuid;
$service->save();
if ($oneClickDotEnvs && $oneClickDotEnvs->count() > 0) {
$oneClickDotEnvs->each(function ($value) use ($service) {
$key = Str::before($value, '=');
$value = Str::of(Str::after($value, '='));
if ($value->contains('SERVICE_USER')) {
$value = Str::of(Str::random(10));
}
if ($value->contains('SERVICE_PASSWORD')) {
$value = Str::of(Str::password(symbols: false));
}
if ($value->contains('SERVICE_BASE64')) {
$length = Str::of($value)->after('SERVICE_BASE64_')->beforeLast('_')->value();
if (is_numeric($length)) {
$length = (int) $length;
} else {
$length = 1;
}
$value = Str::of(base64_encode(Str::password(length: $length, symbols: false)));
}
EnvironmentVariable::create([
'key' => $key,
'value' => $value,
'value' => $value->value(),
'service_id' => $service->id,
'is_build_time' => false,
'is_preview' => false,
]);
});
}
$service->parse(isNew: true);
$service->parse(isNew: true, requiredFqdns: $oneClickRequiredFqdn);
return redirect()->route('project.service', [
'service_uuid' => $service->uuid,

View File

@ -2,6 +2,7 @@
namespace App\Http\Livewire\Project\New;
use App\Models\EnvironmentVariable;
use App\Models\Project;
use App\Models\Service;
use Livewire\Component;
@ -11,6 +12,7 @@
class DockerCompose extends Component
{
public string $dockerComposeRaw = '';
public string $envFile = '';
public array $parameters;
public array $query;
public function mount()
@ -45,13 +47,22 @@ public function submit()
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
$service = Service::create([
'name' => 'service' . Str::random(10),
'docker_compose_raw' => $this->dockerComposeRaw,
'environment_id' => $environment->id,
'server_id' => (int) $server_id,
]);
$variables = parseEnvFormatToArray($this->envFile);
foreach ($variables as $key => $variable) {
EnvironmentVariable::create([
'key' => $key,
'value' => $variable,
'is_build_time' => false,
'is_preview' => false,
'service_id' => $service->id,
]);
}
$service->name = "service-$service->uuid";
$service->parse(isNew: true);
@ -64,6 +75,5 @@ public function submit()
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@ -29,6 +29,7 @@ class Select extends Component
protected $queryString = [
'server',
];
public function mount()
{
$this->parameters = get_route_parameters();
@ -46,6 +47,7 @@ public function mount()
// return handleError($e, $this);
// }
// }
public function loadThings()
{
$this->loadServices();
@ -77,7 +79,6 @@ public function loadServices(bool $forceReload = false)
});
}
$this->services = $cached;
} catch (\Throwable $e) {
ray($e);
return handleError($e, $this);

View File

@ -3,26 +3,29 @@
namespace App\Http\Livewire\Project\Service;
use App\Models\ServiceApplication;
use Illuminate\Support\Collection;
use Livewire\Component;
class Application extends Component
{
public ServiceApplication $application;
public $parameters;
public $fileStorages = null;
public $fileStorages;
protected $listeners = ["refreshFileStorages"];
protected $rules = [
'application.human_name' => 'nullable',
'application.description' => 'nullable',
'application.fqdn' => 'nullable',
'application.image_tag' => 'required',
'application.ignore_from_status' => 'required|boolean',
'application.image' => 'required',
'application.exclude_from_status' => 'required|boolean',
'application.required_fqdn' => 'required|boolean',
];
public function render()
{
return view('livewire.project.service.application');
}
public function instantSave() {
public function instantSave()
{
$this->submit();
}
public function refreshFileStorages()
@ -42,6 +45,7 @@ public function delete()
public function mount()
{
$this->parameters = get_route_parameters();
$this->fileStorages = collect();
$this->refreshFileStorages();
}
public function submit()
@ -49,6 +53,7 @@ public function submit()
try {
$this->validate();
$this->application->save();
switchImage($this->application);
$this->emit('success', 'Application saved successfully.');
} catch (\Throwable $e) {
ray($e);

View File

@ -8,25 +8,34 @@
class Database extends Component
{
public ServiceDatabase $database;
public $fileStorages;
protected $listeners = ["refreshFileStorages"];
protected $rules = [
'database.human_name' => 'nullable',
'database.description' => 'nullable',
'database.image_tag' => 'required',
'database.ignore_from_status' => 'required|boolean',
'database.image' => 'required',
'database.exclude_from_status' => 'required|boolean',
];
public function render()
{
return view('livewire.project.service.database');
}
public function mount() {
$this->refreshFileStorages();
}
public function instantSave() {
$this->submit();
}
public function refreshFileStorages()
{
$this->fileStorages = $this->database->fileStorages()->get();
}
public function submit()
{
try {
$this->validate();
$this->database->save();
switchImage($this->database);
$this->emit('success', 'Database saved successfully.');
} catch (\Throwable $e) {
ray($e);

View File

@ -15,6 +15,7 @@ class FileStorage extends Component
public string $fs_path;
protected $rules = [
'fileStorage.is_directory' => 'required',
'fileStorage.fs_path' => 'required',
'fileStorage.mount_path' => 'required',
'fileStorage.content' => 'nullable',
@ -39,6 +40,9 @@ public function submit()
return handleError($e, $this);
}
}
public function instantSave() {
$this->submit();
}
public function render()
{
return view('livewire.project.service.file-storage');

View File

@ -3,32 +3,57 @@
namespace App\Http\Livewire\Project\Service;
use App\Models\Service;
use App\Models\ServiceApplication;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Livewire\Component;
class Index extends Component
{
use WithRateLimiting;
public Service $service;
public $applications;
public $databases;
public array $parameters;
public array $query;
protected $rules = [
'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required',
'service.name' => 'required',
'service.description' => 'nullable',
];
public function manualRefreshStack() {
try {
$this->rateLimit(5);
$this->refreshStack();
} catch(\Throwable $e) {
return handleError($e, $this);
}
}
public function refreshStack()
{
$this->applications = $this->service->applications->sort();
$this->applications->each(function ($application) {
$application->fileStorages()->get()->each(function ($fileStorage) use ($application) {
if (!$fileStorage->is_directory && $fileStorage->content == null) {
$application->hasMissingFiles = true;
}
});
});
$this->databases = $this->service->databases->sort();
$this->databases->each(function ($database) {
$database->fileStorages()->get()->each(function ($fileStorage) use ($database) {
if (!$fileStorage->is_directory && $fileStorage->content == null) {
$database->hasMissingFiles = true;
}
});
});
$this->emit('success', 'Stack refreshed successfully.');
}
public function mount()
{
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
$this->applications = $this->service->applications->sort();
$this->databases = $this->service->databases->sort();
$this->refreshStack();
}
public function render()
{
@ -43,7 +68,7 @@ public function save()
$this->emit('refreshEnvs');
$this->emit('success', 'Service saved successfully.');
$this->service->saveComposeConfigs();
} catch(\Throwable $e) {
} catch (\Throwable $e) {
return handleError($e, $this);
}
}

View File

@ -39,5 +39,6 @@ public function stop()
{
StopService::run($this->service);
$this->service->refresh();
$this->emit('success', 'Service stopped successfully.');
}
}

View File

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

View File

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

View File

@ -53,11 +53,11 @@ public function saveVariables($isPreview)
$this->resource->environment_variables_preview()->delete();
} else {
$variables = parseEnvFormatToArray($this->variables);
ray($variables);
$existingVariables = $this->resource->environment_variables();
$this->resource->environment_variables()->delete();
}
foreach ($variables as $key => $variable) {
ray($key, $variable);
$found = $existingVariables->where('key', $key)->first();
if ($found) {
$found->value = $variable;
@ -110,11 +110,16 @@ public function submit($data)
$environment->is_build_time = $data['is_build_time'];
$environment->is_preview = $data['is_preview'];
if ($this->resource->type() === 'application') {
$environment->application_id = $this->resource->id;
}
if ($this->resource->type() === 'standalone-postgresql') {
$environment->standalone_postgresql_id = $this->resource->id;
switch ($this->resource->type()) {
case 'application':
$environment->application_id = $this->resource->id;
break;
case 'standalone-postgresql':
$environment->standalone_postgresql_id = $this->resource->id;
break;
case 'service':
$environment->service_id = $this->resource->id;
break;
}
$environment->save();
$this->refreshEnvs();

View File

@ -15,7 +15,7 @@ class Show extends Component
protected $rules = [
'env.key' => 'required|string',
'env.value' => 'required|string',
'env.value' => 'nullable',
'env.is_build_time' => 'required|boolean',
];
protected $validationAttributes = [

View File

@ -151,6 +151,9 @@ public function handle(): void
$subType = data_get($labels, 'coolify.service.subType');
$subId = data_get($labels, 'coolify.service.subId');
$service = $services->where('id', $serviceLabelId)->first();
if (!$service) {
continue;
}
if ($subType === 'application') {
$service = $service->applications()->where('id', $subId)->first();
} else {

View File

@ -96,21 +96,47 @@ public function saveComposeConfigs()
foreach ($envs as $env) {
$commands[] = "echo '{$env->key}={$env->value}' >> .env";
}
if ($envs->count() === 0) {
$commands[] = "touch .env";
}
instant_remote_process($commands, $this->server);
}
private function generateFqdn($serviceVariables, $serviceName)
private function sslip(Server $server)
{
if (isDev()) {
return "127.0.0.1.sslip.io";
}
return "{$server->ip}.sslip.io";
}
private function generateFqdn($serviceVariables, $serviceName, Collection $requiredFqdns)
{
// Add sslip.io to the service
// if (Str::of($serviceVariables)->contains('SERVICE_FQDN') || Str::of($serviceVariables)->contains('SERVICE_URL')) {
$defaultUsableFqdn = "http://$serviceName-{$this->uuid}.{$this->server->ip}.sslip.io";
if (isDev()) {
$defaultUsableFqdn = "http://$serviceName-{$this->uuid}.127.0.0.1.sslip.io";
$defaultUsableFqdn = null;
$sslip = $this->sslip($this->server);
if (Str::of($serviceVariables)->contains('SERVICE_FQDN') || Str::of($serviceVariables)->contains('SERVICE_URL')) {
$defaultUsableFqdn = "http://$serviceName-{$this->uuid}.{$sslip}";
}
if ($requiredFqdns->count() > 0) {
foreach ($requiredFqdns as $requiredFqdn) {
$requiredFqdn = (array)$requiredFqdn;
$name = data_get($requiredFqdn, 'name');
$path = data_get($requiredFqdn, 'path');
$customFqdn = data_get($requiredFqdn, 'customFqdn');
if ($serviceName === $name) {
$defaultUsableFqdn = "http://$serviceName-{$this->uuid}.{$sslip}{$path}";
if ($customFqdn) {
$defaultUsableFqdn = "http://$customFqdn-{$this->uuid}.{$sslip}{$path}";
}
}
}
// }
}
return $defaultUsableFqdn ?? null;
}
public function parse(bool $isNew = false): Collection
public function parse(bool $isNew = false, ?Collection $requiredFqdns = null): Collection
{
if (!$requiredFqdns) {
$requiredFqdns = collect([]);
}
ray('parsing');
// ray()->clearAll();
if ($this->docker_compose_raw) {
@ -130,16 +156,26 @@ public function parse(bool $isNew = false): Collection
$envs = collect([]);
$ports = collect([]);
$services = collect($services)->map(function ($service, $serviceName) use ($composeVolumes, $composeNetworks, $definedNetwork, $envs, $volumes, $ports, $isNew) {
$services = collect($services)->map(function ($service, $serviceName) use ($composeVolumes, $composeNetworks, $definedNetwork, $envs, $volumes, $ports, $isNew, $requiredFqdns) {
$container_name = "$serviceName-{$this->uuid}";
$isDatabase = false;
$serviceVariables = collect(data_get($service, 'environment', []));
// Add env_file with at least .env to the service
$envFile = collect(data_get($service, 'env_file', []));
if ($envFile->count() > 0) {
if (!$envFile->contains('.env')) {
$envFile->push('.env');
}
} else {
$envFile = collect(['.env']);
}
data_set($service, 'env_file', $envFile->toArray());
// Decide if the service is a database
$image = data_get($service, 'image');
if ($image) {
$imageName = Str::of($image)->before(':');
$imageTag = Str::of($image)->after(':') ?? 'latest';
if (collect(DATABASE_DOCKER_IMAGES)->contains($imageName)) {
$isDatabase = true;
data_set($service, 'is_database', true);
@ -160,17 +196,29 @@ public function parse(bool $isNew = false): Collection
if ($isDatabase) {
$savedService = ServiceDatabase::create([
'name' => $serviceName,
'image_tag' => $imageTag,
'image' => $image,
'service_id' => $this->id
]);
} else {
$savedService = ServiceApplication::create([
'name' => $serviceName,
'fqdn' => $this->generateFqdn($serviceVariables, $serviceName),
'image_tag' => $imageTag,
'fqdn' => $this->generateFqdn($serviceVariables, $serviceName, $requiredFqdns),
'image' => $image,
'service_id' => $this->id
]);
}
if ($requiredFqdns->count() > 0) {
$found = false;
foreach ($requiredFqdns as $requiredFqdn) {
$requiredFqdn = (array)$requiredFqdn;
$name = data_get($requiredFqdn, 'name');
if ($serviceName === $name) {
$savedService->required_fqdn = true;
$savedService->save();
break;
}
}
}
} else {
if ($isDatabase) {
$savedService = $this->databases()->whereName($serviceName)->first();
@ -179,14 +227,13 @@ public function parse(bool $isNew = false): Collection
if (data_get($savedService, 'fqdn')) {
$defaultUsableFqdn = data_get($savedService, 'fqdn', null);
} else {
$defaultUsableFqdn = $this->generateFqdn($serviceVariables, $serviceName);
$defaultUsableFqdn = $this->generateFqdn($serviceVariables, $serviceName, $requiredFqdns);
}
$savedService->fqdn = $defaultUsableFqdn;
$savedService->save();
}
}
// Set image tag
$fqdns = data_get($savedService, 'fqdn');
if ($fqdns) {
$fqdns = collect(Str::of($fqdns)->explode(','));
@ -209,6 +256,7 @@ public function parse(bool $isNew = false): Collection
}
$savedService->ports = $collectedPorts->implode(',');
$savedService->save();
// Collect volumes
$serviceVolumes = collect(data_get($service, 'volumes', []));
if ($serviceVolumes->count() > 0) {
@ -341,6 +389,7 @@ public function parse(bool $isNew = false): Collection
$value = Str::after($variable, '=');
if (!Str::startsWith($value, '$SERVICE_') && !Str::startsWith($value, '${SERVICE_') && Str::startsWith($value, '$')) {
$value = Str::of(replaceVariables(Str::of($value)));
$nakedName = $nakedValue = null;
if ($value->contains(':')) {
$nakedName = $value->before(':');
$nakedValue = $value->after(':');
@ -464,6 +513,7 @@ public function parse(bool $isNew = false): Collection
$number = 0;
}
$fqdn = getFqdnWithoutPort(data_get($fqdns, $number, $fqdns->first()));
ray($fqdns);
$environments = collect(data_get($service, 'environment'));
$environments = $environments->map(function ($envValue) use ($value, $fqdn) {
$envValue = Str::of($envValue)->replace($value, $fqdn);
@ -491,6 +541,7 @@ public function parse(bool $isNew = false): Collection
}
}
}
// Add labels to the service
$labels = collect(data_get($service, 'labels', []));
$labels = collect([]);

View File

@ -18,7 +18,6 @@ public function documentation()
{
$services = Cache::get('services', []);
$service = data_get($services, $this->name, []);
ray($this->name);
return data_get($service, 'documentation', 'https://coolify.io/docs');
}
public function service()

View File

@ -3,7 +3,9 @@
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
function replaceRegex(?string $name = null)
{
@ -22,33 +24,41 @@ function serviceStatus(Service $service)
{
$foundRunning = false;
$isDegraded = false;
$foundRestaring = false;
$applications = $service->applications;
$databases = $service->databases;
foreach ($applications as $application) {
if ($application->ignore_from_status) {
if ($application->exclude_from_status) {
continue;
}
if (Str::of($application->status)->startsWith('running')) {
$foundRunning = true;
} else if (Str::of($application->status)->startsWith('restarting')) {
$foundRestaring = true;
} else {
$isDegraded = true;
}
}
foreach ($databases as $database) {
if ($database->ignore_from_status) {
if ($database->exclude_from_status) {
continue;
}
if (Str::of($database->status)->startsWith('running')) {
$foundRunning = true;
} else if (Str::of($database->status)->startsWith('restarting')) {
$foundRestaring = true;
} else {
$isDegraded = true;
}
}
if ($foundRestaring) {
return 'degraded';
}
if ($foundRunning && !$isDegraded) {
return 'running';
} else if ($foundRunning && $isDegraded) {
return 'degraded';
} else if (!$foundRunning && $isDegraded) {
} else if (!$foundRunning && !$isDegraded) {
return 'exited';
}
return 'exited';
@ -60,19 +70,25 @@ function saveFileVolumesHelper(ServiceApplication|ServiceDatabase $oneService)
$server = $oneService->service->server;
$applicationFileVolume = $oneService->fileStorages()->get();
$commands = collect([
"mkdir -p $workdir",
"mkdir -p $workdir > /dev/null 2>&1 || true",
"cd $workdir"
]);
foreach ($applicationFileVolume as $fileVolume) {
$content = $fileVolume->content;
$path = Str::of($fileVolume->fs_path);
if ($fileVolume->is_directory) {
$commands->push("test -f $path && rm -f $path > /dev/null 2>&1 || true");
$commands->push("mkdir -p $path > /dev/null 2>&1 || true");
continue;
}
$content = $fileVolume->content;
$dir = $path->beforeLast('/');
if ($dir->startsWith('.')) {
$dir = $dir->after('.');
$dir = $workdir . $dir;
}
$content = base64_encode($content);
$commands->push("mkdir -p $dir");
$commands->push("test -d $path && rm -rf $path > /dev/null 2>&1 || true");
$commands->push("mkdir -p $dir > /dev/null 2>&1 || true");
$commands->push("echo '$content' | base64 -d > $path");
}
return instant_remote_process($commands, $server);
@ -80,3 +96,18 @@ function saveFileVolumesHelper(ServiceApplication|ServiceDatabase $oneService)
return handleError($e);
}
}
function switchImage($resource)
{
try {
$name = data_get($resource, 'name');
$dockerComposeRaw = data_get($resource, 'service.docker_compose_raw');
$image = data_get($resource, 'image');
$dockerCompose = Yaml::parse($dockerComposeRaw);
data_set($dockerCompose, "services.{$name}.image", $image);
$dockerComposeRaw = Yaml::dump($dockerCompose, 10, 2);
$resource->service->docker_compose_raw = $dockerComposeRaw;
$resource->service->save();
} catch (\Throwable $e) {
return handleError($e);
}
}

View File

@ -12,8 +12,9 @@
public function up(): void
{
Schema::table('service_applications', function (Blueprint $table) {
$table->boolean('ignore_from_status')->default(false);
$table->string('image_tag')->nullable();
$table->boolean('exclude_from_status')->default(false);
$table->boolean('required_fqdn')->default(false);
$table->string('image')->nullable();
});
}
@ -23,8 +24,9 @@ public function up(): void
public function down(): void
{
Schema::table('service_applications', function (Blueprint $table) {
$table->dropColumn('ignore_from_status');
$table->dropColumn('image_tag');
$table->dropColumn('exclude_from_status');
$table->dropColumn('required_fqdn');
$table->dropColumn('image');
});
}
};

View File

@ -12,8 +12,8 @@
public function up(): void
{
Schema::table('service_databases', function (Blueprint $table) {
$table->boolean('ignore_from_status')->default(false);
$table->string('image_tag')->nullable();
$table->boolean('exclude_from_status')->default(false);
$table->string('image')->nullable();
});
}
@ -23,8 +23,8 @@ public function up(): void
public function down(): void
{
Schema::table('service_databases', function (Blueprint $table) {
$table->dropColumn('ignore_from_status');
$table->dropColumn('image_tag');
$table->dropColumn('exclude_from_status');
$table->dropColumn('image');
});
}
};

View File

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

File diff suppressed because one or more lines are too long

View File

@ -11,12 +11,13 @@
</svg>
</a>
</li>
<li title="Send feedback or get help" class="fixed top-0 right-0 p-2 px-4 pt-2 mt-auto">
<div class="justify-center text-xs text-warning" wire:click="help" onclick="help.showModal()">
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M22 5.5H9c-1.1 0-2 .9-2 2v9a2 2 0 0 0 2 2h13c1.11 0 2-.89 2-2v-9a2 2 0 0 0-2-2m0 11H9V9.17l6.5 3.33L22 9.17v7.33m-6.5-5.69L9 7.5h13l-6.5 3.31M5 16.5c0 .17.03.33.05.5H1c-.552 0-1-.45-1-1s.448-1 1-1h4v1.5M3 7h2.05c-.02.17-.05.33-.05.5V9H3c-.55 0-1-.45-1-1s.45-1 1-1m-2 5c0-.55.45-1 1-1h3v2H2c-.55 0-1-.45-1-1Z"/>
<li title="Send feedback or get help" class="fixed top-0 right-0 p-2 px-4 pt-4 mt-auto">
<div class="justify-center" wire:click="help" onclick="help.showModal()">
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M22 5.5H9c-1.1 0-2 .9-2 2v9a2 2 0 0 0 2 2h13c1.11 0 2-.89 2-2v-9a2 2 0 0 0-2-2m0 11H9V9.17l6.5 3.33L22 9.17v7.33m-6.5-5.69L9 7.5h13l-6.5 3.31M5 16.5c0 .17.03.33.05.5H1c-.552 0-1-.45-1-1s.448-1 1-1h4v1.5M3 7h2.05c-.02.17-.05.33-.05.5V9H3c-.55 0-1-.45-1-1s.45-1 1-1m-2 5c0-.55.45-1 1-1h3v2H2c-.55 0-1-.45-1-1Z" />
</svg>
Feedback
Feedback
</div>
</li>
<li class="pb-6" title="Logout">

View File

@ -116,8 +116,8 @@ class="{{ request()->is('settings*') ? 'text-warning icon' : 'icon' }}" viewBox=
@endif
@if (isSubscriptionActive() || isDev())
<li title="Send feedback or get help" class="fixed top-0 right-0 p-2 px-4 pt-4 mt-auto">
<div class="justify-center text-xs text-warning" wire:click="help" onclick="help.showModal()">
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<div class="justify-center" wire:click="help" onclick="help.showModal()">
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M22 5.5H9c-1.1 0-2 .9-2 2v9a2 2 0 0 0 2 2h13c1.11 0 2-.89 2-2v-9a2 2 0 0 0-2-2m0 11H9V9.17l6.5 3.33L22 9.17v7.33m-6.5-5.69L9 7.5h13l-6.5 3.31M5 16.5c0 .17.03.33.05.5H1c-.552 0-1-.45-1-1s.448-1 1-1h4v1.5M3 7h2.05c-.02.17-.05.33-.05.5V9H3c-.55 0-1-.45-1-1s.45-1 1-1m-2 5c0-.55.45-1 1-1h3v2H2c-.55 0-1-.45-1-1Z" />
</svg>

View File

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

View File

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

View File

@ -44,5 +44,6 @@
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQL_ROOT}
'></x-forms.textarea>
<x-forms.textarea label="Environment File" rows="20" id="envFile"></x-forms.textarea>
</form>
</div>

View File

@ -17,22 +17,30 @@
<x-forms.input label="Description" id="application.description"></x-forms.input>
</div>
<div class="flex gap-2">
<x-forms.input placeholder="https://app.coolify.io" label="Domains"
id="application.fqdn"></x-forms.input>
<x-forms.input required helper="You can change the image tag you would like to deploy.<br><br><span class='text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>" label="Image Tag" id="application.image_tag"></x-forms.input>
@if ($application->required_fqdn)
<x-forms.input required placeholder="https://app.coolify.io" label="Domains"
id="application.fqdn"></x-forms.input>
@else
<x-forms.input placeholder="https://app.coolify.io" label="Domains"
id="application.fqdn"></x-forms.input>
@endif
<x-forms.input required
helper="You can change the image you would like to deploy.<br><br><span class='text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"
label="Image" id="application.image"></x-forms.input>
</div>
</div>
<h3 class="pt-2">Advanced</h3>
<div class="w-64">
<x-forms.checkbox instantSave label="Ignore from service status"
<x-forms.checkbox instantSave label="Exclude from service status"
helper="If you do not need to monitor this resource, enable. Useful if this service is optional."
id="application.ignore_from_status"></x-forms.checkbox>
id="application.exclude_from_status"></x-forms.checkbox>
</div>
</form>
@if ($fileStorages->count() > 0)
<h3 class="py-4">Files</h3>
<h3 class="py-4">Mounted Files (binds)</h3>
<div class="flex flex-col gap-4">
@foreach ($fileStorages->get() as $fileStorage)
@foreach ($fileStorages as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="{{ $loop->index }}" />
@endforeach
</div>

View File

@ -15,21 +15,21 @@
<x-forms.input label="Description" id="database.description"></x-forms.input>
</div>
<div class="flex gap-2">
<x-forms.input required helper="You can change the image tag you would like to deploy.<br><br><span class='text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>" label="Image Tag"
id="database.image_tag"></x-forms.input>
<x-forms.input required helper="You can change the image you would like to deploy.<br><br><span class='text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>" label="Image Tag"
id="database.image"></x-forms.input>
</div>
</div>
<h3 class="pt-2">Advanced</h3>
<div class="w-64">
<x-forms.checkbox instantSave label="Ignore from service status"
<x-forms.checkbox instantSave label="Exclude from service status"
helper="If you do not need to monitor this resource, enable. Useful if this service is optional."
id="database.ignore_from_status"></x-forms.checkbox>
id="database.exclude_from_status"></x-forms.checkbox>
</div>
</form>
@if ($database->fileStorages()->get()->count() > 0)
@if ($fileStorages->count() > 0)
<h3 class="py-4">Mounted Files (binds)</h3>
<div class="flex flex-col gap-4">
@foreach ($database->fileStorages()->get() as $fileStorage)
@foreach ($fileStorages as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="{{ $loop->index }}" />
@endforeach
</div>

View File

@ -1,20 +1,26 @@
<x-collapsible>
<x-slot:title>
<div>{{ $fileStorage->mount_path }}
@empty($fileStorage->content)
@if(is_null($fileStorage->content) && !$fileStorage->is_directory)
<span class="text-xs text-warning">(empty)</span>
@endempty
@endif
</div>
</x-slot:title>
<x-slot:action>
<form wire:submit.prevent='submit' class="flex flex-col gap-2">
<div class="flex gap-2">
<x-forms.input readonly label="File in Docker Compose file" id="fileStorage.fs_path"></x-forms.input>
<x-forms.input readonly label="File on Filesystem" id="fs_path"></x-forms.input>
<div class="w-64">
<x-forms.checkbox instantSave label="Is directory?" id="fileStorage.is_directory"></x-forms.checkbox>
</div>
<x-forms.input readonly label="Mount (in container)" id="fileStorage.mount_path"></x-forms.input>
<x-forms.textarea label="Content" rows="20" id="fileStorage.content"></x-forms.textarea>
<x-forms.button type="submit">Save</x-forms.button>
<x-forms.input readonly label="Save your required files here:" id="fs_path"></x-forms.input>
@if (!$fileStorage->is_directory)
<div class="flex gap-2">
<x-forms.input readonly label="File in Docker Compose file" id="fileStorage.fs_path"></x-forms.input>
<x-forms.input readonly label="File on Filesystem" id="fs_path"></x-forms.input>
</div>
<x-forms.input readonly label="Mount (in container)" id="fileStorage.mount_path"></x-forms.input>
<x-forms.textarea label="Content" rows="20" id="fileStorage.content"></x-forms.textarea>
<x-forms.button type="submit">Save</x-forms.button>
@endif
</form>
</x-slot:action>
</x-collapsible>

View File

@ -28,7 +28,10 @@
<x-forms.input id="service.description" label="Description" />
</div>
</form>
<h2 class="pb-4"> Service Stack </h2>
<div class="flex gap-2">
<h2 class="pb-4"> Service Stack </h2>
<x-forms.button wire:click='manualRefreshStack'>Refresh</x-forms.button>
</div>
<div class="grid grid-cols-1 gap-2 xl:grid-cols-3 ">
@foreach ($applications as $application)
<a @class([
@ -46,6 +49,9 @@
@else
{{ Str::headline($application->name) }}
@endif
@if ($application->hasMissingFiles)
<span class="text-xs text-error">(has missing files)</span>
@endif
@if ($application->description)
<span class="text-xs">{{ $application->description }}</span>
@endif
@ -58,11 +64,11 @@
@foreach ($databases as $database)
<a @class([
'border-l border-dashed border-red-500' => Str::of(
$application->status)->contains(['exited']),
$database->status)->contains(['exited']),
'border-l border-dashed border-success' => Str::of(
$application->status)->contains(['running']),
$database->status)->contains(['running']),
'border-l border-dashed border-warning' => Str::of(
$application->status)->contains(['restarting']),
$database->status)->contains(['restarting']),
'flex flex-col justify-center box',
])
href="{{ route('project.service.show', [...$parameters, 'service_name' => $database->name]) }}">
@ -71,10 +77,13 @@
@else
{{ Str::headline($database->name) }}
@endif
@if ($database->hasMissingFiles)
<span class="text-xs text-error">(has missing files)</span>
@endif
@if ($database->description)
<span class="text-xs">{{ $database->description }}</span>
@endif
<div class="text-xs">{{ $application->status }}</div>
<div class="text-xs">{{ $database->status }}</div>
</a>
@endforeach
</div>

View File

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

View File

@ -8,16 +8,7 @@
@else
<x-status.stopped status="Proxy Stopped" />
@endif
<button wire:loading.remove.delay.longer wire:click.prevent='getProxyStatusWithNoti'>
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="#FCD44F">
<path
d="M12.079 3v-.75V3Zm-8.4 8.333h-.75h.75Zm0 1.667l-.527.532a.75.75 0 0 0 1.056 0L3.68 13Zm2.209-1.134A.75.75 0 1 0 4.83 10.8l1.057 1.065ZM2.528 10.8a.75.75 0 0 0-1.056 1.065L2.528 10.8Zm16.088-3.408a.75.75 0 1 0 1.277-.786l-1.277.786ZM12.079 2.25c-5.047 0-9.15 4.061-9.15 9.083h1.5c0-4.182 3.42-7.583 7.65-7.583v-1.5Zm-9.15 9.083V13h1.5v-1.667h-1.5Zm1.28 2.2l1.679-1.667L4.83 10.8l-1.68 1.667l1.057 1.064Zm0-1.065L2.528 10.8l-1.057 1.065l1.68 1.666l1.056-1.064Zm15.684-5.86A9.158 9.158 0 0 0 12.08 2.25v1.5a7.658 7.658 0 0 1 6.537 3.643l1.277-.786Z" />
<path fill="#fff"
d="M11.883 21v.75V21Zm8.43-8.333h.75h-.75Zm0-1.667l.528-.533a.75.75 0 0 0-1.055 0l.528.533ZM18.1 12.133a.75.75 0 1 0 1.055 1.067L18.1 12.133Zm3.373 1.067a.75.75 0 1 0 1.054-1.067L21.473 13.2ZM5.318 16.606a.75.75 0 1 0-1.277.788l1.277-.788Zm6.565 5.144c5.062 0 9.18-4.058 9.18-9.083h-1.5c0 4.18-3.43 7.583-7.68 7.583v1.5Zm9.18-9.083V11h-1.5v1.667h1.5Zm-1.277-2.2L18.1 12.133l1.055 1.067l1.686-1.667l-1.055-1.066Zm0 1.066l1.687 1.667l1.054-1.067l-1.686-1.666l-1.055 1.066Zm-15.745 5.86a9.197 9.197 0 0 0 7.841 4.357v-1.5a7.697 7.697 0 0 1-6.564-3.644l-1.277.788Z"
opacity=".5" />
</g>
</svg></button>
<x-forms.button wire:click='getProxyStatusWithNoti'>Refresh </x-forms.button>
</div>
@endif
</div>