feat: services

This commit is contained in:
Andras Bacsai 2023-09-25 15:48:43 +02:00
parent 58522b59b7
commit 0b11093d18
27 changed files with 314 additions and 87 deletions

View File

@ -14,9 +14,9 @@ public function handle(Service $service)
$commands[] = "cd " . $service->workdir();
$commands[] = "echo '####### Starting service {$service->name} on {$service->server->name}.'";
$commands[] = "echo '####### Pulling images.'";
$commands[] = "docker compose pull --quiet";
$commands[] = "docker compose pull";
$commands[] = "echo '####### Starting containers.'";
$commands[] = "docker compose up -d >/dev/null 2>&1";
$commands[] = "docker compose up -d";
$commands[] = "docker network connect $service->uuid coolify-proxy 2>/dev/null || true";
$activity = remote_process($commands, $service->server);
return $activity;

View File

@ -4,6 +4,9 @@
use App\Models\Project;
use App\Models\Server;
use App\Models\Service;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
class ProjectController extends Controller
{
@ -41,9 +44,10 @@ public function show()
public function new()
{
$type = request()->query('type');
$services = Cache::get('services', []);
$type = Str::of(request()->query('type'));
$destination_uuid = request()->query('destination');
$server = requesT()->query('server');
$server_id = request()->query('server');
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
@ -61,8 +65,28 @@ public function new()
'database_uuid' => $standalone_postgresql->uuid,
]);
}
if ($type->startsWith('one-click-service-')) {
$oneClickServiceName = $type->after('one-click-service-')->value();
$oneClickService = data_get($services, $oneClickServiceName);
if ($oneClickService) {
$service = Service::create([
'name' => "$oneClickServiceName-" . Str::random(10),
'docker_compose_raw' => base64_decode($oneClickService),
'environment_id' => $environment->id,
'server_id' => (int) $server_id,
]);
$service->parse(isNew: true);
return redirect()->route('project.service', [
'service_uuid' => $service->uuid,
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,
]);
}
}
return view('project.new', [
'type' => $type
'type' => $type->value()
]);
}

View File

@ -10,15 +10,16 @@
class DockerCompose extends Component
{
public string $dockercompose = '';
public string $dockerComposeRaw = '';
public array $parameters;
public array $query;
public function mount()
{
$this->parameters = get_route_parameters();
$this->query = request()->query();
if (isDev()) {
$this->dockercompose = 'services:
$this->dockerComposeRaw = 'services:
plausible_events_db:
image: clickhouse/clickhouse-server:23.3.7.5-alpine
restart: always
@ -37,9 +38,9 @@ public function submit()
{
try {
$this->validate([
'dockercompose' => 'required'
'dockerComposeRaw' => 'required'
]);
$this->dockercompose = Yaml::dump(Yaml::parse($this->dockercompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
$this->dockerComposeRaw = Yaml::dump(Yaml::parse($this->dockerComposeRaw), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
$server_id = $this->query['server_id'];
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
@ -47,7 +48,7 @@ public function submit()
$service = Service::create([
'name' => 'service' . Str::random(10),
'docker_compose_raw' => $this->dockercompose,
'docker_compose_raw' => $this->dockerComposeRaw,
'environment_id' => $environment->id,
'server_id' => (int) $server_id,
]);

View File

@ -3,12 +3,12 @@
namespace App\Http\Livewire\Project\New;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Countable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Livewire\Component;
use Route;
use Illuminate\Support\Str;
class Select extends Component
{
@ -21,6 +21,8 @@ class Select extends Component
public Collection|array $standaloneDockers = [];
public Collection|array $swarmDockers = [];
public array $parameters;
public Collection|array $services = [];
public bool $loadingServices = true;
public ?string $existingPostgresqlUrl = null;
@ -44,6 +46,35 @@ public function mount()
// return handleError($e, $this);
// }
// }
public function loadThings()
{
$this->loadServices();
$this->loadServers();
}
public function loadServices(bool $forceReload = false)
{
try {
if ($forceReload) {
Cache::forget('services');
}
$cached = Cache::remember('services', 3600, function () {
$services = Http::get(config('constants.services.offical'));
if ($services->failed()) {
throw new \Exception($services->body());
}
$services = collect($services->json());
$this->emit('success', 'Successfully reloaded services from the internet.');
return $services;
});
$this->services = $cached;
} catch (\Throwable $e) {
ray($e);
return handleError($e, $this);
} finally {
$this->loadingServices = false;
}
}
public function setType(string $type)
{
$this->type = $type;
@ -87,7 +118,7 @@ public function setDestination(string $destination_uuid)
]);
}
public function load_servers()
public function loadServers()
{
$this->servers = Server::isUsable()->get();
}

View File

@ -8,23 +8,39 @@
class Application extends Component
{
public ServiceApplication $application;
public $parameters;
public $fileStorages = null;
protected $listeners = ["refreshFileStorages"];
protected $rules = [
'application.human_name' => 'nullable',
'application.description' => 'nullable',
'application.fqdn' => 'nullable',
'application.ignore_from_status' => 'required|boolean',
];
public function render()
{
return view('livewire.project.service.application');
}
public function instantSave() {
$this->submit();
}
public function refreshFileStorages()
{
$this->fileStorages = $this->application->fileStorages()->get();
}
public function delete()
{
try {
$this->application->delete();
$this->emit('success', 'Application deleted successfully.');
return redirect()->route('project.service', $this->parameters);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function mount()
{
$this->parameters = get_route_parameters();
$this->refreshFileStorages();
}
public function submit()

View File

@ -12,16 +12,22 @@ class Database extends Component
protected $rules = [
'database.human_name' => 'nullable',
'database.description' => 'nullable',
'database.ignore_from_status' => 'required|boolean',
];
public function render()
{
return view('livewire.project.service.database');
}
public function instantSave() {
$this->submit();
}
public function submit()
{
try {
$this->validate();
$this->database->save();
$this->emit('success', 'Database saved successfully.');
} catch (\Throwable $e) {
ray($e);
} finally {

View File

@ -3,14 +3,17 @@
namespace App\Http\Livewire\Project\Service;
use App\Models\Service;
use App\Models\ServiceApplication;
use Livewire\Component;
class Index extends Component
{
public Service $service;
public $applications;
public $databases;
public array $parameters;
public array $query;
protected $rules = [
'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required',
@ -23,6 +26,9 @@ 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();
}
public function render()
{
@ -30,12 +36,16 @@ public function render()
}
public function save()
{
$this->service->save();
$this->service->parse();
$this->service->refresh();
$this->emit('refreshEnvs');
$this->emit('success', 'Service saved successfully.');
$this->service->saveComposeConfigs();
try {
$this->service->save();
$this->service->parse();
$this->service->refresh();
$this->emit('refreshEnvs');
$this->emit('success', 'Service saved successfully.');
$this->service->saveComposeConfigs();
} catch(\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{

View File

@ -76,7 +76,6 @@ public function handle(): void
$databases = $this->server->databases();
$services = $this->server->services();
$previews = $this->server->previews();
$this->server->proxyType();
/// Check if proxy is running
$foundProxyContainer = $containers->filter(function ($value, $key) {
@ -149,19 +148,20 @@ public function handle(): void
}
$serviceLabelId = data_get($labels, 'coolify.serviceId');
if ($serviceLabelId) {
$coolifyName = data_get($labels, 'coolify.name');
$serviceName = Str::of($coolifyName)->before('-');
$serviceUuid = Str::of($coolifyName)->after('-');
$service = $services->where('uuid', $serviceUuid)->first();
$subType = data_get($labels, 'coolify.service.subType');
$subId = data_get($labels, 'coolify.service.subId');
$service = $services->where('id', $serviceLabelId)->first();
if ($subType === 'application') {
$service = $service->applications()->where('id', $subId)->first();
} else {
$service = $service->databases()->where('id', $subId)->first();
}
if ($service) {
$foundService = $service->byName($serviceName);
if ($foundService) {
$foundServices[] = "$foundService->id-$serviceName";
$statusFromDb = $foundService->status;
if ($statusFromDb !== $containerStatus) {
// ray('Updating status: ' . $containerStatus);
$foundService->update(['status' => $containerStatus]);
}
$foundServices[] = "$service->id-$service->name";
$statusFromDb = $service->status;
if ($statusFromDb !== $containerStatus) {
// ray('Updating status: ' . $containerStatus);
$service->update(['status' => $containerStatus]);
}
}
}

View File

@ -79,7 +79,8 @@ public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->orderBy('key', 'asc');
}
public function workdir() {
public function workdir()
{
return service_configuration_dir() . "/{$this->uuid}";
}
public function saveComposeConfigs()
@ -97,6 +98,16 @@ public function saveComposeConfigs()
}
instant_remote_process($commands, $this->server);
}
private function generateFqdn($serviceVariables, $serviceName)
{
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";
}
}
return $defaultUsableFqdn ?? null;
}
public function parse(bool $isNew = false): Collection
{
ray('parsing');
@ -132,20 +143,27 @@ public function parse(bool $isNew = false): Collection
data_set($service, 'is_database', true);
}
}
if ($isNew) {
if ($isDatabase) {
$savedService = ServiceDatabase::where([
'name' => $serviceName,
'service_id' => $this->id
])->first();
} else {
$savedService = ServiceApplication::where([
'name' => $serviceName,
'service_id' => $this->id
])->first();
}
if ($isNew || is_null($savedService)) {
if ($isDatabase) {
$savedService = ServiceDatabase::create([
'name' => $serviceName,
'service_id' => $this->id
]);
} else {
$defaultUsableFqdn = "http://$serviceName-{$this->uuid}.{$this->server->ip}.sslip.io";
if (isDev()) {
$defaultUsableFqdn = "http://$serviceName-{$this->uuid}.127.0.0.1.sslip.io";
}
$savedService = ServiceApplication::create([
'name' => $serviceName,
'fqdn' => $defaultUsableFqdn,
'fqdn' => $this->generateFqdn($serviceVariables, $serviceName),
'service_id' => $this->id
]);
}
@ -157,14 +175,9 @@ public function parse(bool $isNew = false): Collection
if (data_get($savedService, 'fqdn')) {
$defaultUsableFqdn = data_get($savedService, 'fqdn', null);
} else {
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 = $this->generateFqdn($serviceVariables, $serviceName);
}
$savedService->fqdn = $defaultUsableFqdn ?? null;
$savedService->fqdn = $defaultUsableFqdn;
$savedService->save();
}
}
@ -475,7 +488,7 @@ public function parse(bool $isNew = false): Collection
// Add labels to the service
$labels = collect(data_get($service, 'labels', []));
$labels = collect([]);
$labels = $labels->merge(defaultLabels($this->id, $container_name, type: 'service'));
$labels = $labels->merge(defaultLabels($this->id, $container_name, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id));
if (!$isDatabase) {
if ($fqdns) {
$labels = $labels->merge(fqdnLabelsForTraefik($fqdns, $container_name, true));

View File

@ -130,7 +130,7 @@ function get_port_from_dockerfile($dockerfile): int
return 80;
}
function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'application')
function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'application', $subType = null, $subId = null)
{
$labels = collect([]);
$labels->push('coolify.managed=true');
@ -141,6 +141,10 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
if ($pull_request_id !== 0) {
$labels->push('coolify.pullRequestId=' . $pull_request_id);
}
if ($type === 'service') {
$labels->push('coolify.service.subId=' . $subId);
$labels->push('coolify.service.subType=' . $subType);
}
return $labels;
}
function fqdnLabelsForTraefik(Collection $domains, $container_name, $is_force_https_enabled)

View File

@ -25,6 +25,9 @@ function serviceStatus(Service $service)
$applications = $service->applications;
$databases = $service->databases;
foreach ($applications as $application) {
if ($application->ignore_from_status) {
continue;
}
if (Str::of($application->status)->startsWith('running')) {
$foundRunning = true;
} else {
@ -32,6 +35,9 @@ function serviceStatus(Service $service)
}
}
foreach ($databases as $database) {
if ($database->ignore_from_status) {
continue;
}
if (Str::of($database->status)->startsWith('running')) {
$foundRunning = true;
} else {

View File

@ -100,8 +100,7 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.";
}
if (isset($livewire)) {
$livewire->emit('error', $message);
throw new RuntimeException($message);
return $livewire->emit('error', $message);
}
throw new RuntimeException($message);

View File

@ -14,6 +14,9 @@
'expiration' => 10,
],
],
'services' => [
'offical' => 'https://cdn.coollabs.io/coolify/service-templates.json',
],
'limits' => [
'trial_period'=> 7,
'server' => [

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('service_applications', function (Blueprint $table) {
$table->boolean('ignore_from_status')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('service_applications', function (Blueprint $table) {
$table->dropColumn('ignore_from_status');
});
}
};

View File

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

View File

@ -0,0 +1,3 @@
{
"plausible-analytics": "dmVyc2lvbjogIjMuMyIKc2VydmljZXM6CiAgcGxhdXNpYmxlOgogICAgaW1hZ2U6IHBsYXVzaWJsZS9hbmFseXRpY3M6djIuMAogICAgY29tbWFuZDogc2ggLWMgInNsZWVwIDEwICYmIC9lbnRyeXBvaW50LnNoIGRiIGNyZWF0ZWRiICYmIC9lbnRyeXBvaW50LnNoIGRiIG1pZ3JhdGUgJiYgL2VudHJ5cG9pbnQuc2ggcnVuIgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vcG9zdGdyZXM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcGxhdXNpYmxlX2RiL3BsYXVzaWJsZQogICAgICAtIEJBU0VfVVJMPSRTRVJWSUNFX0ZRRE5fUExBVVNJQkxFCiAgICAgIC0gU0VDUkVUX0tFWV9CQVNFPSRTRVJWSUNFX0JBU0U2NF82NF9QTEFVU0lCTEUKICAgIGRlcGVuZHNfb246CiAgICAgIC0gcGxhdXNpYmxlX2RiCiAgICAgIC0gcGxhdXNpYmxlX2V2ZW50c19kYgogICAgICAtIG1haWwKCiAgbWFpbDoKICAgIGltYWdlOiBieXRlbWFyay9zbXRwCgogIHBsYXVzaWJsZV9kYjoKICAgIGltYWdlOiBwb3N0Z3JlczoxNC1hbHBpbmUKICAgIHZvbHVtZXM6CiAgICAgIC0gZGItZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX0RCPXBsYXVzaWJsZQogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCgogIHBsYXVzaWJsZV9ldmVudHNfZGI6CiAgICBpbWFnZTogY2xpY2tob3VzZS9jbGlja2hvdXNlLXNlcnZlcjoyMy4zLjcuNS1hbHBpbmUKICAgIHZvbHVtZXM6CiAgICAgIC0gdHlwZTogdm9sdW1lCiAgICAgICAgc291cmNlOiBldmVudC1kYXRhCiAgICAgICAgdGFyZ2V0OiAvdmFyL2xpYi9jbGlja2hvdXNlCiAgICAgIC0gdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9jbGlja2hvdXNlL2NsaWNraG91c2UtY29uZmlnLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9sb2dnaW5nLnhtbAogICAgICAgIHJlYWRfb25seTogdHJ1ZQogICAgICAgIGNvbnRlbnQ6ID4tCiAgICAgICAgICA8Y2xpY2tob3VzZT48cHJvZmlsZXM+PGRlZmF1bHQ+PGxvZ19xdWVyaWVzPjA8L2xvZ19xdWVyaWVzPjxsb2dfcXVlcnlfdGhyZWFkcz4wPC9sb2dfcXVlcnlfdGhyZWFkcz48L2RlZmF1bHQ+PC9wcm9maWxlcz48L2NsaWNraG91c2U+CiAgICAgIC0gdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9jbGlja2hvdXNlL2NsaWNraG91c2UtdXNlci1jb25maWcueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL3VzZXJzLmQvbG9nZ2luZy54bWwKICAgICAgICByZWFkX29ubHk6IHRydWUKICAgICAgICBjb250ZW50OiA+LQogICAgICAgICAgPGNsaWNraG91c2U+PGxvZ2dlcj48bGV2ZWw+d2FybmluZzwvbGV2ZWw+PGNvbnNvbGU+dHJ1ZTwvY29uc29sZT48L2xvZ2dlcj48cXVlcnlfdGhyZWFkX2xvZwogICAgICAgICAgcmVtb3ZlPSJyZW1vdmUiLz48cXVlcnlfbG9nIHJlbW92ZT0icmVtb3ZlIi8+PHRleHRfbG9nCiAgICAgICAgICByZW1vdmU9InJlbW92ZSIvPjx0cmFjZV9sb2cgcmVtb3ZlPSJyZW1vdmUiLz48bWV0cmljX2xvZwogICAgICAgICAgcmVtb3ZlPSJyZW1vdmUiLz48YXN5bmNocm9ub3VzX21ldHJpY19sb2cKICAgICAgICAgIHJlbW92ZT0icmVtb3ZlIi8+PHNlc3Npb25fbG9nIHJlbW92ZT0icmVtb3ZlIi8+PHBhcnRfbG9nCiAgICAgICAgICByZW1vdmU9InJlbW92ZSIvPjwvY2xpY2tob3VzZT4KICAgIHVsaW1pdHM6CiAgICAgIG5vZmlsZToKICAgICAgICBzb2Z0OiAyNjIxNDQKICAgICAgICBoYXJkOiAyNjIxNDQK"
}

View File

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

View File

@ -1,5 +1,6 @@
@auth
<nav class="fixed h-full overflow-hidden overflow-y-auto scrollbar">
<nav class="fixed h-full overflow-hidden overflow-y-auto pt-14 scrollbar">
<a href="/" class="fixed top-0 z-50 mx-3 mt-3 cursor-pointer bg-coolgray-100"><img class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a>
<ul class="flex flex-col h-full gap-4 menu flex-nowrap">
<li title="Dashboard">
<a class="hover:bg-transparent" @if (!request()->is('/')) href="/" @endif>

View File

@ -1,5 +1,6 @@
@auth
<nav class="fixed h-full overflow-hidden overflow-y-auto pt-14 scrollbar">
<nav class="fixed h-full overflow-hidden overflow-y-auto pt-28 scrollbar">
<a href="/" class="fixed top-0 z-50 mx-3 mt-3 cursor-pointer bg-coolgray-100"><img class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a>
<ul class="flex flex-col h-full gap-4 menu flex-nowrap">
<li title="Dashboard">
<a class="hover:bg-transparent" @if (!request()->is('/')) href="/" @endif>
@ -114,12 +115,13 @@ class="{{ request()->is('settings*') ? 'text-warning icon' : 'icon' }}" viewBox=
</li>
@endif
@if (isSubscriptionActive() || isDev())
<li title="Send feedback or get help" class="fixed top-0 right-0 p-2 px-4 pt-2 mt-auto">
<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">
<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"/>
<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>
@endif

View File

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

View File

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

View File

@ -15,7 +15,7 @@
- SERVICE_BASE64_64_*: Generated 'base64' string with length of '64' (example: SERVICE_BASE64_64_GHOST, to generate 32 bit: SERVICE_BASE64_32_GHOST)<br>
- SERVICE_USER_*: Generated user (example: SERVICE_USER_MYSQL)<br>
- SERVICE_PASSWORD_*: Generated password (example: SERVICE_PASSWORD_MYSQL)<br>"
rows="20" id="dockercompose"
rows="20" id="dockerComposeRaw"
placeholder='services:
ghost:
documentation: https://ghost.org/docs/config

View File

@ -1,4 +1,4 @@
<div x-data x-init="$wire.load_servers">
<div x-data x-init="$wire.loadThings">
<h1>New Resource</h1>
<div class="pb-4 ">Deploy resources, like Applications, Databases, Services...</div>
<div class="flex flex-col gap-2 pt-10">
@ -52,18 +52,16 @@
</div>
</div>
</div>
@if (isDev())
<div class="box group" wire:click="setType('dockercompose')">
<div class="flex flex-col mx-6">
<div class="group-hover:text-white">
Based on a Docker Compose
</div>
<div class="text-xs group-hover:text-white">
You can deploy complex application easily with Docker Compose.
</div>
<div class="box group" wire:click="setType('docker-compose-empty')">
<div class="flex flex-col mx-6">
<div class="group-hover:text-white">
Based on a Docker Compose
</div>
<div class="text-xs group-hover:text-white">
You can deploy complex application easily with Docker Compose.
</div>
</div>
@endif
</div>
</div>
<h2 class="py-4">Databases</h2>
<div class="grid justify-start grid-cols-1 gap-2 text-left xl:grid-cols-3">
@ -88,9 +86,28 @@
</div>
</div> --}}
</div>
<h2 class="py-4">Services</h2>
<div class="flex items-center gap-2">
<h2 class="py-4">Services</h2>
<x-forms.button wire:click='loadServices(true)'>Reload Services from Templates</x-forms.button>
</div>
<div class="grid justify-start grid-cols-1 gap-2 text-left xl:grid-cols-3">
Ghost, Plausible, Wordpress, etc... Coming very very soon...
@if ($loadingServices)
<span class="loading loading-xs loading-spinner"></span>
@else
@foreach ($services as $serviceName => $service)
<div class="box group" wire:click="setType('one-click-service-{{ $serviceName }}')">
<div class="flex flex-col mx-6">
<div class="group-hover:text-white">
{{ Str::headline($serviceName) }}
</div>
<div class="text-xs group-hover:text-white">
</div>
</div>
</div>
@endforeach
@endif
</div>
@endif
@if ($current_step === 'servers')

View File

@ -7,13 +7,22 @@
<h2>{{ Str::headline($application->name) }}</h2>
@endif
<x-forms.button type="submit">Save</x-forms.button>
<x-forms.button isError wire:click='delete'>Delete</x-forms.button>
<a target="_blank" href="{{ $application->documentation() }}">Documentation <x-external-link /></a>
</div>
<div class="flex gap-2">
<x-forms.input label="Name" id="application.human_name" placeholder="Human readable name"></x-forms.input>
<x-forms.input label="Description" id="application.description"></x-forms.input>
<x-forms.input placeholder="https://app.coolify.io" label="Domains"
id="application.fqdn"></x-forms.input>
<div class="flex flex-col gap-2">
<div class="flex gap-2">
<x-forms.input label="Name" id="application.human_name"
placeholder="Human readable name"></x-forms.input>
<x-forms.input label="Description" id="application.description"></x-forms.input>
</div>
<x-forms.input placeholder="https://app.coolify.io" label="Domains" id="application.fqdn"></x-forms.input>
</div>
<h3 class="pt-2">Advanced</h3>
<div class="w-64">
<x-forms.checkbox instantSave label="Ignore 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>
</div>
</form>
@if ($fileStorages->count() > 0)

View File

@ -13,6 +13,12 @@
<x-forms.input label="Name" id="database.human_name" placeholder="Name"></x-forms.input>
<x-forms.input label="Description" id="database.description"></x-forms.input>
</div>
<h3 class="pt-2">Advanced</h3>
<div class="w-64">
<x-forms.checkbox instantSave label="Ignore 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>
</div>
</form>
@if ($database->fileStorages()->get()->count() > 0)
<h3 class="py-4">Mounted Files (binds)</h3>

View File

@ -29,9 +29,17 @@
</div>
</form>
<h2 class="pb-4"> Service Stack </h2>
<div class="grid grid-cols-1 gap-2">
@foreach ($service->applications as $application)
<a class="flex flex-col justify-center box"
<div class="grid grid-cols-1 gap-2 xl:grid-cols-3 ">
@foreach ($applications as $application)
<a @class([
'border-l border-dashed border-red-500' => Str::of(
$application->status)->contains(['exited']),
'border-l border-dashed border-success' => Str::of(
$application->status)->contains(['running']),
'border-l border-dashed border-warning' => Str::of(
$application->status)->contains(['restarting']),
'flex flex-col justify-center box',
])
href="{{ route('project.service.show', [...$parameters, 'service_name' => $application->name]) }}">
@if ($application->human_name)
{{ Str::headline($application->human_name) }}
@ -44,10 +52,19 @@
@if ($application->fqdn)
<span class="text-xs">{{ $application->fqdn }}</span>
@endif
<div class="text-xs">{{ $application->status }}</div>
</a>
@endforeach
@foreach ($service->databases as $database)
<a class="flex flex-col justify-center box"
@foreach ($databases as $database)
<a @class([
'border-l border-dashed border-red-500' => Str::of(
$application->status)->contains(['exited']),
'border-l border-dashed border-success' => Str::of(
$application->status)->contains(['running']),
'border-l border-dashed border-warning' => Str::of(
$application->status)->contains(['restarting']),
'flex flex-col justify-center box',
])
href="{{ route('project.service.show', [...$parameters, 'service_name' => $database->name]) }}">
@if ($database->human_name)
{{ Str::headline($database->human_name) }}
@ -57,6 +74,7 @@
@if ($database->description)
<span class="text-xs">{{ $database->description }}</span>
@endif
<div class="text-xs">{{ $application->status }}</div>
</a>
@endforeach
</div>
@ -90,7 +108,8 @@
</x-forms.textarea>
</div>
<div x-cloak x-show="raw === false">
<x-forms.textarea label="Actual Docker Compose file that will be deployed" readonly rows="20" id="service.docker_compose">
<x-forms.textarea label="Actual Docker Compose file that will be deployed" readonly
rows="20" id="service.docker_compose">
</x-forms.textarea>
</div>
</div>

View File

@ -7,7 +7,7 @@
<livewire:project.new.github-private-repository-deploy-key :type="$type" />
@elseif ($type === 'dockerfile')
<livewire:project.new.simple-dockerfile :type="$type" />
@elseif ($type === 'dockercompose')
@elseif ($type === 'docker-compose-empty')
<livewire:project.new.docker-compose :type="$type" />
@else
<livewire:project.new.select />