Refactor code and update UI components

This commit is contained in:
Andras Bacsai 2024-03-22 11:34:15 +01:00
parent ca9a2cb13a
commit 8b7e1e4169
51 changed files with 592 additions and 609 deletions

View File

@ -37,6 +37,7 @@ ## Github Sponsors ($40+)
<a href="https://cryptojobslist.com/?utm_source=coolify.io"><img src="https://github.com/cryptojobslist.png" width="60px" alt="CryptoJobsList" /></a>
<a href="https://typebot.io/?utm_source=coolify.io"><img src="https://pbs.twimg.com/profile_images/1509194008366657543/9I-C7uWT_400x400.jpg" width="60px" alt="typebot"/></a>
<a href="https://bc.direct"><img width="60px" alt="BC Direct" src="https://github.com/coollabsio/coolify/assets/5845193/a4063c41-95ed-4a32-8814-cd1475572e37"/></a>
<a href="https://www.uxwizz.com/?utm_source=coolify.io"><img width="60px" alt="UXWizz" src="https://github.com/UXWizz.png"/></a>
<a href="https://github.com/automazeio"><img src="https://github.com/automazeio.png" width="60px" alt="Corentin Clichy" /></a>
<a href="https://github.com/corentinclichy"><img src="https://github.com/corentinclichy.png" width="60px" alt="Corentin Clichy" /></a>
<a href="https://github.com/Niki2k1"><img src="https://github.com/Niki2k1.png" width="60px" alt="Niklas Lausch" /></a>

View File

@ -13,6 +13,7 @@ class ActivityMonitor extends Component
public $activityId;
public $eventToDispatch = 'activityFinished';
public $isPollingActive = false;
public bool $showWaiting = false;
protected $activity;
protected $listeners = ['activityMonitor' => 'newMonitorActivity'];

View File

@ -5,7 +5,7 @@
use App\Models\Server;
use App\Models\StandaloneDocker as ModelsStandaloneDocker;
use App\Models\SwarmDocker;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@ -14,7 +14,7 @@ class Docker extends Component
public string $name;
public string $network;
public Collection $servers;
public ?Collection $servers = null;
public Server $server;
public ?int $server_id = null;
public bool $is_swarm = false;
@ -34,6 +34,9 @@ class Docker extends Component
public function mount()
{
if (is_null($this->servers)) {
$this->servers = Server::ownedByCurrentTeam()->get();
}
if (request()->query('server_id')) {
$this->server_id = request()->query('server_id');
} else {

View File

@ -3,6 +3,8 @@
namespace App\Livewire\Destination;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Illuminate\Support\Collection;
use Livewire\Component;
@ -11,6 +13,40 @@ class Show extends Component
public Server $server;
public Collection|array $networks = [];
private function createNetworkAndAttachToProxy()
{
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
}
public function add($name)
{
if ($this->server->isSwarm()) {
$found = $this->server->swarmDockers()->where('network', $name)->first();
if ($found) {
$this->dispatch('error', 'Network already added to this server.');
return;
} else {
$docker = SwarmDocker::create([
'name' => $this->server->name . "-" . $name,
'network' => $this->name,
'server_id' => $this->server->id,
]);
}
} else {
$found = $this->server->standaloneDockers()->where('network', $name)->first();
if ($found) {
$this->dispatch('error', 'Network already added to this server.');
return;
} else {
$docker = StandaloneDocker::create([
'name' => $this->server->name . "-" . $name,
'network' => $name,
'server_id' => $this->server->id,
]);
}
$this->createNetworkAndAttachToProxy();
}
}
public function scan()
{
if ($this->server->isSwarm()) {
@ -26,6 +62,8 @@ public function scan()
});
if ($this->networks->count() === 0) {
$this->dispatch('success', 'No new networks found.');
}
return;
}
$this->dispatch('success', 'Scan done.');
}
}

View File

@ -17,14 +17,6 @@ public function testEvent()
{
$this->dispatch('success', 'Realtime events configured!');
}
public function disableSponsorship()
{
auth()->user()->update(['is_notification_sponsorship_enabled' => false]);
}
public function disableNotifications()
{
auth()->user()->update(['is_notification_notifications_enabled' => false]);
}
public function render()
{
return view('livewire.layout-popups');

View File

@ -2,6 +2,7 @@
namespace App\Livewire\Project;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
use Livewire\Component;
@ -10,7 +11,9 @@ class Index extends Component
{
public $projects;
public $servers;
public $private_keys;
public function mount() {
$this->private_keys = PrivateKey::ownedByCurrentTeam()->get();
$this->projects = Project::ownedByCurrentTeam()->get();
$this->servers = Server::ownedByCurrentTeam()->count();
}

View File

@ -26,7 +26,7 @@ public function getListeners()
];
}
public function serviceStarted() {
$this->dispatch('success', 'Service started.');
$this->dispatch('success', 'Service status changed.');
}
public function serviceStatusChanged()
{

View File

@ -59,7 +59,7 @@ public function submit()
$this->validate();
$this->application->save();
updateCompose($this->application);
$this->dispatch('success', 'Application saved.');
$this->dispatch('success', 'Service saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {

View File

@ -53,7 +53,7 @@ class ByIp extends Component
public function mount()
{
$this->name = generate_random_name();
$this->private_key_id = $this->private_keys->first()->id;
$this->private_key_id = $this->private_keys->first()?->id;
$this->swarm_managers = Server::isUsable()->get()->where('settings.is_swarm_manager', true);
if ($this->swarm_managers->count() > 0) {
$this->selected_swarm_cluster = $this->swarm_managers->first()->id;

View File

@ -30,7 +30,7 @@ public function mount () {
}
public function render()
{
return view('livewire.settings.license')->layout('layouts.subscription');
return view('livewire.settings.license');
}
public function submit()
{

View File

@ -177,9 +177,6 @@ public function isAnyNotificationEnabled()
if (isCloud()) {
return true;
}
if (!data_get(auth()->user(), 'is_notification_notifications_enabled')) {
return true;
}
if ($this->smtp_enabled || $this->resend_enabled || $this->discord_enabled || $this->telegram_enabled || $this->use_instance_email_settings) {
return true;
}

View File

@ -13,7 +13,6 @@ class Button extends Component
*/
public function __construct(
public bool $disabled = false,
public bool $isModal = false,
public bool $noStyle = false,
public ?string $modalId = null,
public string $defaultClass = "button"

View File

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

View File

@ -84,7 +84,7 @@ input {
}
.input {
@apply block w-full py-1.5 rounded border-0 text-sm ring-inset ring-1 dark:bg-coolgray-100 dark:text-white text-black focus:ring-2 dark:focus:ring-coolgray-300 dark:ring-coolgray-300 dark:read-only:text-neutral-500 dark:read-only:ring-0 dark:read-only:bg-coolgray-100/40 dark:placeholder:text-neutral-700;
@apply block w-full py-1.5 pr-[2.8rem] rounded border-0 text-sm ring-inset ring-1 dark:bg-coolgray-100 dark:text-white text-black focus:ring-2 dark:focus:ring-coolgray-300 dark:ring-coolgray-300 dark:read-only:text-neutral-500 dark:read-only:ring-0 dark:read-only:bg-coolgray-100/40 dark:placeholder:text-neutral-700;
}
option {
@ -100,7 +100,7 @@ .alert-error {
}
.dropdown-item {
@apply relative flex cursor-pointer select-none dark:hover:text-white dark:hover:bg-coollabs items-center pr-4 pl-2 py-1 text-xs justify-center outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 gap-2 w-full;
@apply relative flex cursor-pointer select-none dark:text-white dark:hover:bg-coollabs items-center pr-4 pl-2 py-1 text-xs justify-start outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 gap-2 w-full;
}
.badge {

View File

@ -5,7 +5,7 @@
Coolify
</a>
<div
class="w-full bg-white rounded shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-base dark:border-coolgray-200">
class="w-full bg-white shadow md:mt-0 sm:max-w-md xl:p-0 dark:bg-base ">
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
<form action="/login" method="POST" class="flex flex-col gap-2">
@csrf
@ -15,6 +15,7 @@ class="w-full bg-white rounded shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dar
<x-forms.input value="password" type="password" name="password" required
label="{{ __('input.password') }}" />
<a href="/forgot-password" class="text-xs">
{{ __('auth.forgot_password') }}?
</a>
@ -28,8 +29,7 @@ class="w-full bg-white rounded shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dar
@endenv
<x-forms.button class="mt-10" type="submit">{{ __('auth.login') }}</x-forms.button>
@if ($is_registration_enabled)
<a href="/register"
class="button bg-coollabs-gradient">
<a href="/register" class="button bg-coollabs-gradient">
{{ __('auth.register_now') }}
</a>
@endif

View File

@ -3,9 +3,9 @@
}" class="relative" @click.outside="dropdownOpen = false">
<button @click="dropdownOpen=true"
class="inline-flex items-center justify-center py-1 pr-12 text-sm font-medium transition-colors focus:outline-none disabled:opacity-50 disabled:pointer-events-none">
<span class="flex flex-col items-start flex-shrink-0 h-full ml-2 leading-none translate-y-px">
Open Application
class="inline-flex items-center justify-start py-1 pr-10 text-sm font-medium transition-colors focus:outline-none disabled:opacity-50 disabled:pointer-events-none">
<span class="flex flex-col items-start flex-shrink-0 h-full leading-none translate-y-px">
Links
</span>
<svg class="absolute right-0 w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor">

View File

@ -6,7 +6,7 @@
x-transition:enter-start="-translate-y-10" x-transition:enter-end="translate-y-0"
x-transition:leave="transition ease-in duration-100" x-transition:leave-start="translate-y-0"
x-transition:leave-end="-translate-y-10" x-init="setTimeout(() => { bannerVisible = true }, bannerVisibleAfter);"
class="relative z-50 w-full py-2 mx-auto duration-100 ease-out shadow-sm bg-coolgray-100 sm:py-0 sm:h-14" x-cloak>
class="relative z-[999] w-full py-2 mx-auto duration-100 ease-out shadow-sm bg-coolgray-100 sm:py-0 sm:h-14" x-cloak>
<div class="flex items-center justify-between h-full px-3">
{{ $slot }}
@if ($closable)

View File

@ -6,14 +6,10 @@
@isset($confirmAction)
x-on:{{ explode('(', $confirmAction)[0] }}.window="$wire.{{ explode('(', $confirmAction)[0] }}"
@endisset
@if ($isModal) onclick="{{ $modalId }}.showModal()" @endif>
>
{{ $slot }}
@if ($attributes->get('type') === 'submit')
<x-loading wire:target="submit" wire:loading.delay />
@else
@if ($attributes->whereStartsWith('wire:click')->first())
@if ($attributes->whereStartsWith('wire:click')->first() && $attributes->get('type') === 'submit')
<x-loading wire:target="{{ $attributes->whereStartsWith('wire:click')->first() }}" wire:loading.delay />
@endif
@endif
</button>

View File

@ -25,12 +25,12 @@ class="absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer hover:te
</svg>
</div>
@endif
<input x-cloak x-show="type" value="{{ $value }}"
{{ $attributes->merge(['class' => $defaultClass]) }} @required($required)
<input value="{{ $value }}" {{ $attributes->merge(['class' => $defaultClass]) }} @required($required)
@if ($id !== 'null') wire:model={{ $id }} @endif
wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300' wire:dirty.class="dark:focus:ring-warning dark:ring-warning"
wire:loading.attr="disabled" type="{{ $type }}" @readonly($readonly) @disabled($disabled)
id="{{ $id }}" name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300'
wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled"
type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $id }}"
name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}"
aria-placeholder="{{ $attributes->get('placeholder') }}">
</div>
@ -38,8 +38,9 @@ class="absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer hover:te
<input @if ($value) value="{{ $value }}" @endif
{{ $attributes->merge(['class' => $defaultClass]) }} @required($required) @readonly($readonly)
@if ($id !== 'null') wire:model={{ $id }} @endif
wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300' wire:dirty.class="dark:focus:ring-warning dark:ring-warning"
wire:loading.attr="disabled" type="{{ $type }}" @disabled($disabled)
wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300'
wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled"
type="{{ $type }}" @disabled($disabled)
@if ($id !== 'null') id={{ $id }} @endif name="{{ $name }}"
placeholder="{{ $attributes->get('placeholder') }}">
@endif

View File

@ -2,6 +2,7 @@
'title' => 'Are you sure?',
'buttonTitle' => 'Open Modal',
'isErrorButton' => false,
'buttonFullWidth' => false,
'disabled' => false,
'action' => 'delete',
'content' => null,
@ -14,19 +15,37 @@ class="relative w-auto h-auto">
</div>
@else
@if ($disabled)
@if ($buttonFullWidth)
<x-forms.button class="w-full" isError disabled>
{{ $buttonTitle }}
</x-forms.button>
@else
<x-forms.button isError disabled>
{{ $buttonTitle }}
</x-forms.button>
@endif
@elseif ($isErrorButton)
@if ($buttonFullWidth)
<x-forms.button class="w-full" isError @click="modalOpen=true">
{{ $buttonTitle }}
</x-forms.button>
@else
<x-forms.button isError @click="modalOpen=true">
{{ $buttonTitle }}
</x-forms.button>
@endif
@else
@if ($buttonFullWidth)
<x-forms.button @click="modalOpen=true" class="flex w-full gap-2">
{{ $buttonTitle }}
</x-forms.button>
@else
<x-forms.button @click="modalOpen=true" class="flex gap-2">
{{ $buttonTitle }}
</x-forms.button>
@endif
@endif
@endif
<template x-teleport="body">
<div x-show="modalOpen" class="fixed top-0 left-0 z-[99] flex items-center justify-center w-screen h-screen"
x-cloak>

View File

@ -1,216 +0,0 @@
@auth
<nav class="h-screen pt-[4.5rem] border-r scrollbar bg-coolgray-100/40 border-r-coolgray-200">
{{-- <div class="px-2 pb-2" id="vue">
<magic-bar></magic-bar>
</div> --}}
<a href="/" class="fixed top-0 z-50 mx-3 mt-3 bg-transparent cursor-pointer"><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">
<a title="Dashboard" href="/" class="{{ request()->is('/') ? 'menu-item-active menu-item' : 'menu-item' }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
Dashboard
</a>
<a title="Projects"
class="{{ request()->is('project/*') || request()->is('projects') ? 'menu-item menu-item-active' : 'menu-item' }}"
href="/projects">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" 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="M12 4l-8 4l8 4l8 -4l-8 -4" />
<path d="M4 12l8 4l8 -4" />
<path d="M4 16l8 4l8 -4" />
</svg>
Projects
</a>
<a title="Servers"
class="{{ request()->is('server/*') || request()->is('servers') ? 'menu-item menu-item-active' : 'menu-item' }}"
href="/servers">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" 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="M3 4m0 3a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v2a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3z" />
<path d="M15 20h-9a3 3 0 0 1 -3 -3v-2a3 3 0 0 1 3 -3h12" />
<path d="M7 8v.01" />
<path d="M7 16v.01" />
<path d="M20 15l-2 3h3l-2 3" />
</svg>
Servers
</a>
<a title="Security" class="{{ request()->is('security*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('security.private-key.index') }}">
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="m16.555 3.843l3.602 3.602a2.877 2.877 0 0 1 0 4.069l-2.643 2.643a2.877 2.877 0 0 1-4.069 0l-.301-.301l-6.558 6.558a2 2 0 0 1-1.239.578L5.172 21H4a1 1 0 0 1-.993-.883L3 20v-1.172a2 2 0 0 1 .467-1.284l.119-.13L4 17h2v-2h2v-2l2.144-2.144l-.301-.301a2.877 2.877 0 0 1 0-4.069l2.643-2.643a2.877 2.877 0 0 1 4.069 0zM15 9h.01" />
</svg>
Security
</a>
<a title="Source" class="{{ request()->is('source*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('source.all') }}">
<svg class="icon" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="m6.793 1.207l.353.354l-.353-.354ZM1.207 6.793l-.353-.354l.353.354Zm0 1.414l.354-.353l-.354.353Zm5.586 5.586l-.354.353l.354-.353Zm1.414 0l-.353-.354l.353.354Zm5.586-5.586l.353.354l-.353-.354Zm0-1.414l-.354.353l.354-.353ZM8.207 1.207l.354-.353l-.354.353ZM6.44.854L.854 6.439l.707.707l5.585-5.585L6.44.854ZM.854 8.56l5.585 5.585l.707-.707l-5.585-5.585l-.707.707Zm7.707 5.585l5.585-5.585l-.707-.707l-5.585 5.585l.707.707Zm5.585-7.707L8.561.854l-.707.707l5.585 5.585l.707-.707Zm0 2.122a1.5 1.5 0 0 0 0-2.122l-.707.707a.5.5 0 0 1 0 .708l.707.707ZM6.44 14.146a1.5 1.5 0 0 0 2.122 0l-.707-.707a.5.5 0 0 1-.708 0l-.707.707ZM.854 6.44a1.5 1.5 0 0 0 0 2.122l.707-.707a.5.5 0 0 1 0-.708L.854 6.44Zm6.292-4.878a.5.5 0 0 1 .708 0L8.56.854a1.5 1.5 0 0 0-2.122 0l.707.707Zm-2 1.293l1 1l.708-.708l-1-1l-.708.708ZM7.5 5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 6V5Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 4.5H8ZM7.5 4a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 3v1Zm0-1A1.5 1.5 0 0 0 6 4.5h1a.5.5 0 0 1 .5-.5V3Zm.646 2.854l1.5 1.5l.707-.708l-1.5-1.5l-.707.708ZM10.5 8a.5.5 0 0 1-.5-.5H9A1.5 1.5 0 0 0 10.5 9V8Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 12 7.5h-1Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 10.5 6v1Zm0-1A1.5 1.5 0 0 0 9 7.5h1a.5.5 0 0 1 .5-.5V6ZM7 5.5v4h1v-4H7Zm.5 5.5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 12v-1Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 10.5H8Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 9v1Zm0-1A1.5 1.5 0 0 0 6 10.5h1a.5.5 0 0 1 .5-.5V9Z" />
</svg>
Sources
</a>
<a title="Notifications"
class="{{ request()->is('notifications*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('notification.index') }}">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M10 5a2 2 0 1 1 4 0a7 7 0 0 1 4 6v3a4 4 0 0 0 2 3H4a4 4 0 0 0 2-3v-3a7 7 0 0 1 4-6M9 17v1a3 3 0 0 0 6 0v-1" />
</svg>
Notifications
</a>
<a title="Tags" class="{{ request()->is('tags*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('tags.index') }}">
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path
d="M3 8v4.172a2 2 0 0 0 .586 1.414l5.71 5.71a2.41 2.41 0 0 0 3.408 0l3.592-3.592a2.41 2.41 0 0 0 0-3.408l-5.71-5.71A2 2 0 0 0 9.172 6H5a2 2 0 0 0-2 2" />
<path d="m18 19l1.592-1.592a4.82 4.82 0 0 0 0-6.816L15 6m-8 4h-.01" />
</g>
</svg>
Tags
</a>
<a title="Command Center"
class="{{ request()->is('command-center*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('command-center') }}">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" 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="M5 7l5 5l-5 5" />
<path d="M12 19l7 0" />
</svg>
Command Center
</a>
<a title="Profile" class="{{ request()->is('profile*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('profile') }}">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" 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="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M12 10m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" />
<path d="M6.168 18.849a4 4 0 0 1 3.832 -2.849h4a4 4 0 0 1 3.834 2.855" />
</svg>
Profile
</a>
<a title="Teams" class="{{ request()->is('team*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('team.index') }}">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" 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="M10 13a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
<path d="M8 21v-1a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v1" />
<path d="M15 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
<path d="M17 10h2a2 2 0 0 1 2 2v1" />
<path d="M5 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
<path d="M3 13v-1a2 2 0 0 1 2 -2h2" />
</svg>
Teams
</a>
@if (isCloud())
<a title="Subscription"
class="{{ request()->is('subscription*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('subscription.show') }}">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M3 8a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3zm0 2h18M7 15h.01M11 15h2" />
</svg>
Subscription
</a>
@endif
@if (isInstanceAdmin())
<a title="Settings" class="{{ request()->is('settings*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="/settings">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" 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="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" />
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
</svg>
Settings
</a>
@endif
<a title="Onboarding" class="{{ request()->is('onboarding*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('onboarding') }}">
<svg class="icon" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M224 128a8 8 0 0 1-8 8h-88a8 8 0 0 1 0-16h88a8 8 0 0 1 8 8m-96-56h88a8 8 0 0 0 0-16h-88a8 8 0 0 0 0 16m88 112h-88a8 8 0 0 0 0 16h88a8 8 0 0 0 0-16M82.34 42.34L56 68.69L45.66 58.34a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32 0l32-32a8 8 0 0 0-11.32-11.32m0 64L56 132.69l-10.34-10.35a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32 0l32-32a8 8 0 0 0-11.32-11.32m0 64L56 196.69l-10.34-10.35a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32 0l32-32a8 8 0 0 0-11.32-11.32" />
</svg>
Onboarding
</a>
{{-- <div class="menu-item" x-data="{ open: false }">
<div>
<button x-on:click.prevent="open = !open" x-on:click.away="open = false" type="button" id="menu-button"
aria-expanded="true" aria-haspopup="true">
<svg class="icon text-neutral-400" xmlns="http://www.w3.org/2000/svg" width="200" height="200"
viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
<div x-show="open" x-cloak
class="absolute left-0 z-10 w-56 mx-4 mt-2 origin-top-right rounded shadow-lg bg-coolgray-100 ring-1 ring-black ring-opacity-5 focus:outline-none"
role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1">
<div class="py-1" role="none">
</div>
</div>
</div> --}}
@if (isCloud() && isInstanceAdmin())
<a title="Admin" class="menu-item" href="/admin">
<svg class="text-pink-600 icon" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M177.62 159.6a52 52 0 0 1-34 34a12.2 12.2 0 0 1-3.6.55a12 12 0 0 1-3.6-23.45a28 28 0 0 0 18.32-18.32a12 12 0 0 1 22.9 7.2ZM220 144a92 92 0 0 1-184 0c0-28.81 11.27-58.18 33.48-87.28a12 12 0 0 1 17.9-1.33l19.69 19.11L127 19.89a12 12 0 0 1 18.94-5.12C168.2 33.25 220 82.85 220 144m-24 0c0-41.71-30.61-78.39-52.52-99.29l-20.21 55.4a12 12 0 0 1-19.63 4.5L80.71 82.36C67 103.38 60 124.06 60 144a68 68 0 0 0 136 0" />
</svg>
Admin
</a>
@endif
<div class="flex-1"></div>
@if (isInstanceAdmin() && !isCloud())
@persist('upgrade')
<livewire:upgrade />
@endpersist
@endif
<a title="Sponsor us" class="menu-item" href="https://coolify.io/sponsorships" target="_blank">
<svg class="text-pink-500 icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M19.5 12.572L12 20l-7.5-7.428A5 5 0 1 1 12 6.006a5 5 0 1 1 7.5 6.572" />
<path
d="M12 6L8.707 9.293a1 1 0 0 0 0 1.414l.543.543c.69.69 1.81.69 2.5 0l1-1a3.182 3.182 0 0 1 4.5 0l2.25 2.25m-7 3l2 2M15 13l2 2" />
</g>
</svg>
Sponsor us
</a>
<div title="Send us feedback or get help!" class="menu-item" wire:click="help" onclick="help.showModal()">
<svg class="icon" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M140 180a12 12 0 1 1-12-12a12 12 0 0 1 12 12M128 72c-22.06 0-40 16.15-40 36v4a8 8 0 0 0 16 0v-4c0-11 10.77-20 24-20s24 9 24 20s-10.77 20-24 20a8 8 0 0 0-8 8v8a8 8 0 0 0 16 0v-.72c18.24-3.35 32-17.9 32-35.28c0-19.85-17.94-36-40-36m104 56A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88" />
</svg>
Feedback
</div>
<form action="/logout" method="POST" class="mb-6 menu-item">
@csrf
<button title="Logout" type="submit" class="flex gap-2">
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2a9.985 9.985 0 0 1 8 4h-2.71a8 8 0 1 0 .001 12h2.71A9.985 9.985 0 0 1 12 22m7-6v-3h-8v-2h8V8l5 4z" />
</svg>
Logout
</button>
</form>
</ul>
</nav>
@endauth

View File

@ -70,7 +70,7 @@ class="{{ request()->is('security*') ? 'menu-item-active menu-item' : 'menu-item
</a>
</li>
<li>
<a title="Source"
<a title="Sources"
class="{{ request()->is('source*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('source.all') }}">
<svg class="icon" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg">
@ -80,6 +80,17 @@ class="{{ request()->is('source*') ? 'menu-item-active menu-item' : 'menu-item'
Sources
</a>
</li>
<li>
<a title="Destinations"
class="{{ request()->is('destination*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('destination.all') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 4L3 8v12l6-3l6 3l6-4V4l-6 3l-6-3zm-2 8.001V12m4 .001V12m3-2l2 2m2 2l-2-2m0 0l2-2m-2 2l-2 2"/>
</svg>
Destinations
</a>
</li>
<li>
<a title="Notifications"
class="{{ request()->is('notifications*') ? 'menu-item-active menu-item' : 'menu-item' }}"
@ -239,7 +250,7 @@ class="{{ request()->is('onboarding*') ? 'menu-item-active menu-item' : 'menu-it
<x-modal-input title="How can we help?">
<x-slot:content>
<div title="Send us feedback or get help!" class="cursor-pointer menu-item"
wire:click="help" onclick="help.showModal()">
wire:click="help">
<svg class="icon" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M140 180a12 12 0 1 1-12-12a12 12 0 0 1 12 12M128 72c-22.06 0-40 16.15-40 36v4a8 8 0 0 0 16 0v-4c0-11 10.77-20 24-20s24 9 24 20s-10.77 20-24 20a8 8 0 0 0-8 8v8a8 8 0 0 0 16 0v-.72c18.24-3.35 32-17.9 32-35.28c0-19.85-17.94-36-40-36m104 56A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88" />

View File

@ -11,8 +11,10 @@ class="fixed bottom-0 right-0 w-full h-auto duration-300 ease-out sm:px-5 sm:pb-
class="flex flex-col items-center justify-between w-full h-full max-w-4xl p-6 mx-auto bg-white border-t shadow-lg dark:bg-coolgray-100 lg:p-8 lg:flex-row sm:border-0 sm:rounded-xl">
<div
class="flex flex-col items-start h-full pb-6 text-xs lg:items-center lg:flex-row lg:pb-0 lg:pr-6 lg:space-x-5 dark:text-neutral-300">
<img src="https://cdn-icons-png.flaticon.com/512/8236/8236748.png"
class="w-8 h-8 sm:w-12 sm:h-12 lg:w-16 lg:h-16">
@if (isset($icon))
{{ $icon }}
@endif
<div class="pt-6 lg:pt-0">
<h4 class="w-full mb-1 text-xl font-bold leading-none -translate-y-1 text-neutral-900 dark:text-white">
{{ $title }}
@ -21,7 +23,10 @@ class="w-8 h-8 sm:w-12 sm:h-12 lg:w-16 lg:h-16">
</div>
</div>
<div class="flex items-end justify-end w-full pl-3 space-x-3 lg:flex-shrink-0 lg:w-auto">
<button @click="bannerVisible=false;" {{ $buttonText->attributes }}
<button
@if ($buttonText->attributes->whereStartsWith('@click')->first()) @click="bannerVisible=false;{{ $buttonText->attributes->get('@click') }}"
@else
@click="bannerVisible=false;" @endif
class="inline-flex items-center justify-center flex-shrink-0 w-1/2 px-4 py-2 text-sm font-medium tracking-wide transition-colors duration-200 bg-white rounded-md dark:bg-coolgray-200 lg:w-auto dark:text-neutral-200 dark:hover:bg-coolgray-300 focus:shadow-outline focus:outline-none">
{{ $buttonText }}
</button>

View File

@ -3,9 +3,9 @@
dropdownOpen: false
}" class="relative" @click.outside="dropdownOpen = false">
<button @click="dropdownOpen=true"
class="inline-flex items-center justify-center py-1 pr-12 text-sm font-medium transition-colors focus:outline-none disabled:opacity-50 disabled:pointer-events-none">
<span class="flex flex-col items-start flex-shrink-0 h-full ml-2 leading-none translate-y-px">
Open Application
class="inline-flex items-center justify-start py-1 pr-10 text-sm font-medium transition-colors focus:outline-none disabled:opacity-50 disabled:pointer-events-none">
<span class="flex flex-col items-start flex-shrink-0 h-full leading-none translate-y-px">
Links
</span>
<svg class="absolute right-0 w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor">

View File

@ -2,7 +2,6 @@
title: 'Default Toast Notification',
description: '',
type: 'default',
position: 'top-center',
expanded: false,
popToast(custom) {
let html = '';
@ -14,7 +13,7 @@
}" x-init="window.toast = function(message, options = {}) {
let description = '';
let type = 'default';
let position = 'top-center';
let position = 'bottom-right';
let html = '';
if (typeof options.description != 'undefined') description = options.description;
if (typeof options.type != 'undefined') type = options.type;
@ -30,7 +29,7 @@
timeout: null,
expanded: false,
layout: 'default',
position: 'top-center',
position: '',
paddingBetweenToasts: 15,
deleteToastWithId(id) {
for (let i = 0; i < this.toasts.length; i++) {

View File

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

View File

@ -1,6 +1,16 @@
<x-layout>
<div class="flex items-start gap-2">
<h1>Destinations</h1>
<div class="subtitle ">All Destinations.</div>
<x-slide-over fullScreen closeWithX>
<x-slot:title>New Destination</x-slot:title>
<x-slot:content>
<livewire:destination.new.docker :server_id="$server_id" />
</x-slot:content>
<button @click="slideOverOpen=true" class="button">+
Add</button>
</x-slide-over>
</div>
<div class="subtitle">Endpoints to deploy your resources.</div>
<div class="grid gap-2 lg:grid-cols-2">
@forelse ($destinations as $destination)
@if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
@ -23,7 +33,6 @@
@empty
<div>
<div>No destinations found.</div>
<x-use-magic-bar />
</div>
@endforelse
</div>

View File

@ -1,3 +0,0 @@
<x-layout>
<livewire:destination.new.docker :servers="$servers" :server_id="$server_id" />
</x-layout>

View File

@ -8,7 +8,7 @@
<livewire:realtime-connection />
@endauth
@auth
<div x-data="{ open: false }" x-cloak>
<div x-data="{ open: false }" x-cloak class="mx-auto max-w-7xl">
<div class="relative z-50 lg:hidden" :class="open ? 'block' : 'hidden'" role="dialog" aria-modal="true">
<div class="fixed inset-0 bg-black/80"></div>
<div class="fixed inset-0 flex">

View File

@ -38,9 +38,8 @@
@section('body')
<body>
@livewire('wire-elements-modal')
{{-- @livewire('wire-elements-modal') --}}
<x-toast />
{{-- <x-version class="fixed left-7 bottom-1" /> --}}
<script data-navigate-once>
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia(
'(prefers-color-scheme: dark)').matches)) {
@ -89,9 +88,11 @@ function changePasswordFieldType(event) {
if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
if (element.type === 'password') {
element.type = 'text';
element.classList.add('truncate');
this.type = 'text';
} else {
element.type = 'password';
element.classList.remove('truncate');
this.type = 'password';
}
}
@ -232,10 +233,6 @@ function copyToClipboard(text) {
})
}
})
window.Livewire.on('installDocker', () => {
console.log('Installing Docker...');
installDocker.showModal();
})
});
</script>
</body>

View File

@ -14,7 +14,7 @@ class="flex flex-col-reverse w-full px-4 py-2 overflow-y-auto text-white border
<pre class="font-mono whitespace-pre-wrap" @if ($isPollingActive) wire:poll.1000ms="polling" @endif>{{ RunRemoteProcess::decodeOutput($this->activity) }}</pre>
</div>
@else
@if (isset($showWaiting))
@if ($showWaiting)
<x-loading text="Waiting..." />
@endif
@endif

View File

@ -16,9 +16,9 @@
</div>
@endif
<h3 class="pb-4">Projects</h3>
@if ($projects->count() > 0)
<div class="grid grid-cols-1 gap-2 xl:grid-cols-2">
@forelse ($projects as $project)
@foreach ($projects as $project)
<div class="gap-2 border border-transparent cursor-pointer box group">
@if (data_get($project, 'environments')->count() === 1)
<a class="flex flex-col flex-1 mx-6 hover:no-underline"
@ -55,9 +55,12 @@ class="p-2 font-bold group-hover:dark:text-white group-hover:text-black dark:hov
</a>
</div>
</div>
@empty
<div class="flex gap-1">
<span class='font-bold text-warning'>No projects found.</span> Add your first project
@endforeach
</div>
@else
<div class="flex flex-col gap-1">
<div class='font-bold text-warning'>No projects found.</div>
<div class="flex gap-1">Add your first project
<div>
<x-slide-over fullScreen closeWithX>
<x-slot:title>New Project</x-slot:title>
@ -70,11 +73,13 @@ class="p-2 font-bold group-hover:dark:text-white group-hover:text-black dark:hov
</div> or
go to the <a class="underline dark:text-white" href="{{ route('onboarding') }}">onboarding</a> page.
</div>
@endforelse
</div>
@endif
<h3 class="py-4">Servers</h3>
@if ($servers->count() > 0)
<div class="grid grid-cols-1 gap-2 xl:grid-cols-2">
@forelse ($servers as $server)
@foreach ($servers as $server)
<a href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}"
@class([
'gap-2 border cursor-pointer box group',
@ -101,10 +106,13 @@ class="p-2 font-bold group-hover:dark:text-white group-hover:text-black dark:hov
</div>
<div class="flex-1"></div>
</a>
@empty
@endforeach
</div>
@else
@if ($private_keys->count() === 0)
<div class="flex gap-1">
<span class='font-bold text-warning'>No private keys found.</span> Before you can add your server, first add a private key
<div class="flex flex-col gap-1">
<div class='font-bold text-warning'>No private keys found.</div>
<div class="flex gap-1">Before you can add your server, first add a private key
<div>
<x-slide-over fullScreen closeWithX>
<x-slot:title>New Private Key</x-slot:title>
@ -118,6 +126,7 @@ class="p-2 font-bold group-hover:dark:text-white group-hover:text-black dark:hov
go to the <a class="underline dark:text-white" href="{{ route('onboarding') }}">onboarding</a>
page.
</div>
</div>
@else
<div class="flex gap-1">
<span class='font-bold text-warning'>No servers found.</span> Add your first server
@ -135,8 +144,7 @@ class="p-2 font-bold group-hover:dark:text-white group-hover:text-black dark:hov
page.
</div>
@endif
@endforelse
</div>
@endif
<div class="flex items-center gap-2">
<h3 class="py-4">Deployments</h3>
@if (count($deployments_per_server) > 0)

View File

@ -1,18 +1,18 @@
<div>
<h1>Create a new Destination</h1>
<div class="subtitle">Destinations are used to segregate resources by network.</div>
<form class="flex flex-col gap-4" wire:submit='submit'>
<div class="flex gap-2">
<x-forms.input id="name" label="Name" required />
<x-forms.input id="network" label="Network" required />
</div>
@if ($server_id)
<x-forms.select id="server_id" label="Select a server" required wire:change="generate_name">
<option disabled>Select a server</option>
@foreach ($servers as $server)
<option value="{{ $server->id }}">{{ $server->name }}</option>
@endforeach
</x-forms.select>
{{-- <x-forms.checkbox type="checkbox" id="is_swarm" label="Is it a Swarm network?" /> --}}
@endif
<x-forms.button type="submit">
Continue
</x-forms.button>

View File

@ -2,24 +2,28 @@
@if ($server->isFunctional())
<div class="flex items-end gap-2">
<h2>Destinations</h2>
<a href="{{ route('destination.new', ['server_id' => $server->id]) }}">
<x-forms.button>Add a new destination</x-forms.button>
</a>
<x-forms.button wire:click='scan'>Scan destinations on the server</x-forms.button>
<x-slide-over>
<x-slot:title>New Destination</x-slot:title>
<x-slot:content>
<livewire:destination.new.docker :server_id="$server->id" />
</x-slot:content>
<button @click="slideOverOpen=true" class="button">+
Add</button>
</x-slide-over>
<x-forms.button wire:click='scan'>Scan Destinations</x-forms.button>
</div>
<div class="pt-2 pb-6 ">Destinations are used to segregate resources by network.</div>
<div class="flex gap-2 ">
Available for using:
@forelse ($server->standaloneDockers as $docker)
<a
href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">
<a href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">
<button class="text-white btn-link">{{ data_get($docker, 'network') }} </button>
</a>
@empty
@endforelse
@forelse ($server->swarmDockers as $docker)
<a
href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">
<a href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">
<button class="text-white btn-link">{{ data_get($docker, 'network') }} </button>
</a>
@empty
@ -31,12 +35,9 @@
@endif
<div class="flex flex-wrap gap-2 ">
@foreach ($networks as $network)
<div>
<a
href="{{ route('destination.new', ['server_id' => $server->id, 'network_name' => data_get($network, 'Name')]) }}">
<x-forms.button>+<x-highlighted text="{{ data_get($network, 'Name') }}" />
</x-forms.button>
</a>
<div class="min-w-fit">
<x-forms.button wire:click="add('{{ data_get($network, 'Name') }}')">Add
{{ data_get($network, 'Name') }}</x-forms.button>
</div>
@endforeach
</div>

View File

@ -1,9 +1,22 @@
<div>
@if (data_get(auth()->user(), 'is_notification_sponsorship_enabled'))
<div x-data="{
popups: {
sponsorship: true,
notification: true
},
init() {
this.popups.sponsorship = localStorage.getItem('popupSponsorship') !== 'false';
this.popups.notification = localStorage.getItem('popupNotification') !== 'false';
}
}">
<span x-show="popups.sponsorship">
<x-popup>
<x-slot:title>
Love Coolify as we do?
</x-slot:title>
<x-slot:icon>
<img src="https://cdn-icons-png.flaticon.com/512/8236/8236748.png"
class="w-8 h-8 sm:w-12 sm:h-12 lg:w-16 lg:h-16">
</x-slot:icon>
<x-slot:description>
<span>Please
consider donating on <a href="https://github.com/sponsors/coollabsio"
@ -12,23 +25,11 @@ class="text-xs text-white underline">OpenCollective</a>.<br><br></span>
<span>It enables us to keep creating features without paywalls, ensuring our work remains free and
open.</span>
</x-slot:description>
<x-slot:button-text wire:click='disableSponsorship'>
<x-slot:button-text @click="disableSponsorship()">
Disable This Popup
</x-slot:button-text>
</x-popup>
{{-- <div class="toast">
<div class="flex flex-col text-white rounded alert-error bg-coolgray-200">
<span>Love Coolify as we do? <a href="https://coolify.io/sponsorships"
class="underline text-warning">Please
consider donating!</a>💜</span>
<span>It enables us to keep creating features without paywalls, ensuring our work remains free and
open.</span>
<x-forms.button class="bg-coolgray-400" wire:click='disableSponsorship'>Disable This
Popup</x-forms.button>
</div>
</div> --}}
@endif
{{-- <x-popup /> --}}
</span>
@if (currentTeam()->serverOverflow())
<x-banner :closable=false>
<div><span class="font-bold text-red-500">WARNING:</span> The number of active servers exceeds the limit
@ -39,17 +40,38 @@ class="text-white underline">/subscription</a> to update your subscription or re
</x-banner>
@endif
@if (!currentTeam()->isAnyNotificationEnabled())
<div class="toast">
<div class="flex flex-col text-white rounded alert-error bg-coolgray-200">
<span><span class="font-bold text-red-500">WARNING:</span> No notifications enabled.<br><br> It is
<span x-show="popups.notification">
<x-popup>
<x-slot:title>
No notifications enabled.
</x-slot:title>
<x-slot:icon>
<svg xmlns="http://www.w3.org/2000/svg" class="text-red-500 stroke-current w-14 h-14 shrink-0"
fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</x-slot:icon>
<x-slot:description>
It is
highly recommended to enable at least
one
notification channel to receive important alerts.<br>Visit <a
href="{{ route('notification.index') }}" class="text-white underline">/notification</a> to
enable notifications.</span>
<x-forms.button class="bg-coolgray-400" wire:click='disableNotifications'>Disable This
Popup</x-forms.button>
</div>
</div>
</x-slot:description>
<x-slot:button-text @click="disableNotification()">
Accept and Close
</x-slot:button-text>
</x-popup>
</span>
@endif
<script>
function disableSponsorship() {
localStorage.setItem('popupSponsorship', false);
}
function disableNotification() {
localStorage.setItem('popupNotification', false);
}
</script>
</div>

View File

@ -5,20 +5,20 @@
href="{{ route('project.application.configuration', $parameters) }}">
<button>Configuration</button>
</a>
@if (!$application->destination->server->isSwarm())
<a class="{{ request()->routeIs('project.application.command') ? 'text-white' : '' }}"
href="{{ route('project.application.command', $parameters) }}">
<button>Execute Command</button>
</a>
@endif
<a class="{{ request()->routeIs('project.application.logs') ? 'text-white' : '' }}"
href="{{ route('project.application.logs', $parameters) }}">
<button>Logs</button>
</a>
<a class="{{ request()->routeIs('project.application.deployment.index') ? 'text-white' : '' }}"
href="{{ route('project.application.deployment.index', $parameters) }}">
<button>Deployments</button>
</a>
<a class="{{ request()->routeIs('project.application.logs') ? 'text-white' : '' }}"
href="{{ route('project.application.logs', $parameters) }}">
<button>Logs</button>
</a>
@if (!$application->destination->server->isSwarm())
<a class="{{ request()->routeIs('project.application.command') ? 'text-white' : '' }}"
href="{{ route('project.application.command', $parameters) }}">
<button>Command</button>
</a>
@endif
<x-applications.links :application="$application" />
<div class="flex-1"></div>
@if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))

View File

@ -1,10 +1,10 @@
<div>
<form wire:submit="submit">
<div class="flex items-end gap-2">
<x-forms.input id="filename" label="Filename" />
<x-forms.button type="submit">Save</x-forms.button>
<x-forms.button isError wire:click.prevent="delete">Delete</x-forms.button>
<x-modal-confirmation isErrorButton buttonTitle="Delete">
This script will be deleted. It is not reversible. <br>Please think again.
</x-modal-confirmation>
</div>
<x-forms.textarea id="content" label="Content" />
</form>
</div>

View File

@ -82,7 +82,21 @@
<div class="pb-16">
<div class="flex gap-2 pt-4 pb-2">
<h3>Initialization scripts</h3>
<x-forms.button onclick="newInitScript.showModal()">+ Add</x-forms.button>
<x-slide-over>
<x-slot:title>New Init Script</x-slot:title>
<x-slot:content>
<form class="flex flex-col gap-2 rounded modal-box" wire:submit='save_new_init_script'>
<x-forms.input placeholder="create_test_db.sql" id="new_filename" label="Filename" required />
<x-forms.textarea placeholder="CREATE DATABASE test;" id="new_content" label="Content"
required />
<x-forms.button type="submit">
Save
</x-forms.button>
</form>
</x-slot:content>
<button @click="slideOverOpen=true" class="button">+
Add</button>
</x-slide-over>
</div>
<div class="flex flex-col gap-2">
@forelse(data_get($database,'init_scripts', []) as $script)

View File

@ -11,13 +11,8 @@
</x-slide-over>
</div>
<div class="subtitle">All your projects are here.</div>
<div class="grid gap-2 lg:grid-cols-2">
@if ($servers === 0)
<div>
<div>No servers found. Without a server, you won't be able to do much.</div>
<x-use-magic-bar link="/servers" />
</div>
@else
@forelse ($projects as $project)
<div class="gap-2 border border-transparent cursor-pointer box group" x-data
x-on:click="goto('{{ $project->uuid }}')">
@ -46,7 +41,7 @@
<div>No project found.</div>
</div>
@endforelse
@endif
</div>
<script>
function goto(uuid) {
@ -54,4 +49,3 @@ function goto(uuid) {
}
</script>
</div>
</div>

View File

@ -12,11 +12,6 @@
<div class="pb-4 ">Deploy resources, like Applications, Databases, Services...</div>
<div class="flex flex-col gap-4 pt-10">
@if ($current_step === 'type')
{{-- <ul class="pb-10 steps">
<li class="step step-secondary">Select Resource Type</li>
<li class="step">Select a Server</li>
<li class="step">Select a Destination</li>
</ul> --}}
<h2>Applications</h2>
<div class="grid justify-start grid-cols-1 gap-4 text-left xl:grid-cols-3">
<x-resource-view wire="setType('public')">
@ -252,20 +247,8 @@
companies, and use of them does not imply any affiliation or endorsement.</div>
@endif
@if ($current_step === 'servers')
{{-- <ul class="pb-10 steps">
<li class="step step-secondary">Select Resource Type</li>
<li class="step step-secondary">Select a Server</li>
<li class="step">Select a Destination</li>
</ul> --}}
{{-- @if ($isDatabase)
<div class="flex items-center justify-center pt-4">
<x-forms.checkbox instantSave wire:model="includeSwarm"
helper="Swarm clusters are excluded from this list by default. For database, services or complex compose deployments with databases to work with Swarm,
you need to set a few things on the server. Read more <a class='text-white underline' href='https://coolify.io/docs/docker/swarm#database-requirements' target='_blank'>here</a>."
label="Include Swarm Clusters" />
</div>
@endif --}}
<h2>Select a server</h2>
<div class="pb-5"></div>
<div class="flex flex-col justify-center gap-4 text-left xl:flex-row xl:flex-wrap">
@forelse($servers as $server)
<div class="w-64 box group" wire:click="setServer({{ $server }})">
@ -282,8 +265,6 @@
<div>No validated & reachable servers found. <a class="text-white underline" href="/servers">
Go to servers page
</a></div>
<x-use-magic-bar link="/servers" />
</div>
@endforelse
</div>
@ -293,12 +274,9 @@
@endif --}}
@endif
@if ($current_step === 'destinations')
{{-- <ul class="pb-10 steps">
<li class="step step-secondary">Select Resource Type</li>
<li class="step step-secondary">Select a Server</li>
<li class="step step-secondary">Select a Destination</li>
</ul> --}}
<h2>Select a destination</h2>
<div>Destinations are used to segregate resources by network. If you are unsure, select the default
Standalone Docker (coolify).</div>
<div class="flex flex-col justify-center gap-4 text-left xl:flex-row xl:flex-wrap">
@if ($server->isSwarm())
@foreach ($swarmDockers as $swarmDocker)
@ -323,14 +301,6 @@
</div>
@endforeach
@endif
<a href="{{ route('destination.new', ['server_id' => $server_id]) }}"
class="items-center justify-center text-center box-without-bg group bg-coollabs hover:bg-coollabs-100">
<div class="flex flex-col mx-6 ">
<div class="font-bold text-white">
+ Add New
</div>
</div>
</a>
</div>
@endif
@if ($current_step === 'existing-postgresql')

View File

@ -18,7 +18,6 @@
</x-slot:content>
<button @click="slideOverOpen=true" class="button">+ Add</button>
</x-slide-over>
{{-- <x-forms.button onclick="newStorage.showModal()">+ Add</x-forms.button> --}}
</div>
<div class="pb-4">Persistent storage to preserve data between deployments.</div>
@if ($resource->persistentStorages()->get()->count() === 0 && $resource->fileStorages()->get()->count() == 0)

View File

@ -1,7 +1,8 @@
<form class="flex flex-col gap-2 rounded" wire:submit='submit'>
<x-forms.input placeholder="NODE_ENV" id="key" label="Name" required />
<x-forms.textarea x-show="$wire.is_multiline === true" x-cloak id="value" label="Value" required />
<x-forms.input x-show="$wire.is_multiline === false" x-cloak placeholder="production" id="value" x-bind:label="$wire.is_multiline === false && 'Value'" required />
<x-forms.input x-show="$wire.is_multiline === false" x-cloak placeholder="production" id="value"
x-bind:label="$wire.is_multiline === false && 'Value'" required />
@if (data_get($parameters, 'application_uuid'))
<x-forms.checkbox id="is_build_time" label="Build Variable?" />
@endif

View File

@ -1,4 +1,4 @@
<div class="flex flex-col gap-2">
<div class="flex flex-col gap-4">
<div>
<div class="flex items-center gap-2">
<h2>Environment Variables</h2>

View File

@ -1,25 +1,31 @@
<div>
<form wire:submit='submit'
class="flex flex-col gap-2 p-4 m-2 border lg:items-center border-coolgray-300 lg:m-0 lg:p-0 lg:border-0 lg:flex-row">
<form wire:submit='submit' class="flex flex-col items-center gap-4 p-4 border lg:items-start border-coolgray-300">
@if ($isLocked)
<div class="flex flex-1 w-full gap-2">
<x-forms.input disabled id="env.key" />
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M5 13a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-6z" />
<path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0-2 0m-3-5V7a4 4 0 1 1 8 0v4" />
</g>
</svg>
<x-forms.input disabled id="env.key" />
<x-modal-confirmation isErrorButton buttonTitle="Delete">
You will delete environment variable <span
class="font-bold text-warning">{{ $env->key }}</span>.
</x-modal-confirmation>
</div>
@else
@if ($isDisabled)
<div class="flex flex-col w-full gap-2 lg:flex-row">
<x-forms.input disabled id="env.key" />
<x-forms.input disabled type="password" id="env.value" />
@if ($env->is_shared)
<x-forms.input disabled type="password" id="env.real_value" />
@endif
@if ($type !== 'service' && !$isSharedVariable)
<x-forms.checkbox instantSave id="env.is_build_time" label="Build Variable?" />
@endif
</div>
@else
<div class="flex flex-col w-full gap-2 lg:flex-row">
@if ($env->is_multiline)
<x-forms.input isMultiline="{{ $env->is_multiline }}" id="env.key" />
<x-forms.textarea type="password" id="env.value" />
@ -29,21 +35,15 @@ class="flex flex-col gap-2 p-4 m-2 border lg:items-center border-coolgray-300 lg
@endif
@if ($env->is_shared)
<x-forms.input disabled type="password" id="env.real_value" />
@else
<x-forms.checkbox instantSave id="env.is_multiline" label="Is Multiline?" />
@endif
</div>
@endif
<div class="flex flex-col w-full gap-2 lg:flex-row">
@if ($type !== 'service' && !$isSharedVariable)
<x-forms.checkbox instantSave id="env.is_multiline" label="Is Multiline?" />
<x-forms.checkbox instantSave id="env.is_build_time" label="Build Variable?" />
@endif
@endif
@endif
<div class="flex gap-2">
@if ($isLocked)
<x-modal-confirmation isErrorButton buttonTitle="Delete">
You will delete environment variable <span
class="font-bold text-warning">{{ $env->key }}</span>.
</x-modal-confirmation>
@else
<div class="flex-1"></div>
@if ($isDisabled)
<x-forms.button disabled type="submit">
Update
@ -62,12 +62,13 @@ class="font-bold text-warning">{{ $env->key }}</span>.
<x-forms.button wire:click='lock'>
Lock
</x-forms.button>
<x-modal-confirmation isErrorButton buttonTitle="Delete">
<x-modal-confirmation buttonFullWidth isErrorButton buttonTitle="Delete">
You will delete environment variable <span
class="font-bold text-warning">{{ $env->key }}</span>.
</x-modal-confirmation>
@endif
@endif
</div>
@endif
</form>
</div>

View File

@ -2,12 +2,12 @@
@if ($type === 'application')
<h1>Execute Command</h1>
<livewire:project.application.heading :application="$resource" />
<h2 class="pt-4">Command Details</h2>
<h2 class="pt-4">Command</h2>
<div class="pb-2">Run any one-shot command inside a container.</div>
@elseif ($type === 'database')
<h1>Execute Command</h1>
<livewire:project.database.heading :database="$resource" />
<h2 class="pt-4">Command Details</h2>
<h2 class="pt-4">Command</h2>
<div class="pb-2">Run any one-shot command inside a container.</div>
@elseif ($type === 'service')
<h2>Execute Command</h2>

View File

@ -29,7 +29,7 @@
<h1>Subscription</h1>
</div>
<div>You are not an admin or have been removed from this team. If this does not make sense, please <span
class="text-white underline cursor-pointer" wire:click="help" onclick="help.showModal()">contact
class="text-white underline cursor-pointer" wire:click="help">contact
us</span>.</div>
</div>
@endif

View File

@ -10,7 +10,7 @@
Add</button>
</x-slide-over>
</div>
<div class="subtitle ">All Sources.</div>
<div class="subtitle ">Git sources for your applications.</div>
<div class="grid gap-2 lg:grid-cols-2">
@forelse ($sources as $source)
@if ($source->getMorphClass() === 'App\Models\GithubApp')

View File

@ -219,17 +219,11 @@
Route::middleware(['auth'])->group(function () {
Route::get('/destinations', function () {
$servers = Server::all();
$servers = Server::isUsable()->get();
$destinations = collect([]);
foreach ($servers as $server) {
$destinations = $destinations->merge($server->destinations());
}
return view('destination.all', [
'destinations' => $destinations,
]);
})->name('destination.all');
Route::get('/destination/new', function () {
$servers = Server::isUsable()->get();
$pre_selected_server_uuid = data_get(request()->query(), 'server');
if ($pre_selected_server_uuid) {
$server = $servers->firstWhere('uuid', $pre_selected_server_uuid);
@ -237,11 +231,26 @@
$server_id = $server->id;
}
}
return view('destination.new', [
return view('destination.all', [
'destinations' => $destinations,
"servers" => $servers,
"server_id" => $server_id ?? null,
]);
})->name('destination.new');
})->name('destination.all');
// Route::get('/destination/new', function () {
// $servers = Server::isUsable()->get();
// $pre_selected_server_uuid = data_get(request()->query(), 'server');
// if ($pre_selected_server_uuid) {
// $server = $servers->firstWhere('uuid', $pre_selected_server_uuid);
// if ($server) {
// $server_id = $server->id;
// }
// }
// return view('destination.new', [
// "servers" => $servers,
// "server_id" => $server_id ?? null,
// ]);
// })->name('destination.new');
Route::get('/destination/{destination_uuid}', function () {
$standalone_dockers = StandaloneDocker::where('uuid', request()->destination_uuid)->first();
$swarm_dockers = SwarmDocker::where('uuid', request()->destination_uuid)->first();

View File

@ -0,0 +1,31 @@
# documentation: https://shlink.io/
# slogan:
# tags: links, shortener, sharing, url, short, link, sharing
# port: 8080
services:
shlink:
image: shlinkio/shlink:stable
environment:
- SERVICE_FQDN_SHLINK_8080
- DEFAULT_DOMAIN=${SERVICE_URL_SHLINK}
- IS_HTTPS_ENABLED=false
- INITIAL_API_KEY=${SERVICE_BASE64_SHLINKAPIKEY}
volumes:
- shlink-data:/etc/shlink/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/rest/v3/health"]
interval: 2s
timeout: 10s
retries: 15
shlink-web:
image: shlinkio/shlink-web-client
environment:
- SERVICE_FQDN_SHLINKWEB_8080
- SHLINK_SERVER_API_KEY=${SERVICE_BASE64_SHLINKAPIKEY}
- SHLINK_SERVER_URL=${SERVICE_FQDN_SHLINK}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080"]
interval: 2s
timeout: 10s
retries: 15

View File

@ -0,0 +1,17 @@
# documentation: https://github.com/yourselfhosted/slash
# slogan: An open source, self-hosted links shortener and sharing platform.
# tags: links, shortener, sharing, url, short, link, sharing
# port: 5231
services:
slash:
image: yourselfhosted/slash
environment:
- SERVICE_FQDN_SLASH_5231
volumes:
- slash-data:/var/opt/slash
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:5231"]
interval: 2s
timeout: 10s
retries: 15

View File

@ -668,6 +668,40 @@
"logo": "svgs\/posthog.svg",
"minversion": "4.0.0-beta.222"
},
"shlink": {
"documentation": "https:\/\/shlink.io\/",
"slogan": "",
"compose": "c2VydmljZXM6CiAgc2hsaW5rOgogICAgaW1hZ2U6ICdzaGxpbmtpby9zaGxpbms6c3RhYmxlJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1NITElOS184MDgwCiAgICAgIC0gJ0RFRkFVTFRfRE9NQUlOPSR7U0VSVklDRV9VUkxfU0hMSU5LfScKICAgICAgLSBJU19IVFRQU19FTkFCTEVEPWZhbHNlCiAgICAgIC0gJ0lOSVRJQUxfQVBJX0tFWT0ke1NFUlZJQ0VfQkFTRTY0X1NITElOS0FQSUtFWX0nCiAgICB2b2x1bWVzOgogICAgICAtICdzaGxpbmstZGF0YTovZXRjL3NobGluay9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgwODAvcmVzdC92My9oZWFsdGgnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBzaGxpbmstd2ViOgogICAgaW1hZ2U6IHNobGlua2lvL3NobGluay13ZWItY2xpZW50CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fU0hMSU5LV0VCXzgwODAKICAgICAgLSAnU0hMSU5LX1NFUlZFUl9BUElfS0VZPSR7U0VSVklDRV9CQVNFNjRfU0hMSU5LQVBJS0VZfScKICAgICAgLSAnU0hMSU5LX1NFUlZFUl9VUkw9JHtTRVJWSUNFX0ZRRE5fU0hMSU5LfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo4MDgwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==",
"tags": [
"links",
"shortener",
"sharing",
"url",
"short",
"link",
"sharing"
],
"logo": "svgs\/unknown.svg",
"minversion": "0.0.0",
"port": "8080"
},
"slash": {
"documentation": "https:\/\/github.com\/yourselfhosted\/slash",
"slogan": "An open source, self-hosted links shortener and sharing platform.",
"compose": "c2VydmljZXM6CiAgc2xhc2g6CiAgICBpbWFnZTogeW91cnNlbGZob3N0ZWQvc2xhc2gKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9TTEFTSF81MjMxCiAgICB2b2x1bWVzOgogICAgICAtICdzbGFzaC1kYXRhOi92YXIvb3B0L3NsYXNoJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjUyMzEnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK",
"tags": [
"links",
"shortener",
"sharing",
"url",
"short",
"link",
"sharing"
],
"logo": "svgs\/unknown.svg",
"minversion": "0.0.0",
"port": "5231"
},
"snapdrop": {
"documentation": "https:\/\/github.com\/RobinLinus\/snapdrop",
"slogan": "A self-hosted file-sharing service for secure and convenient file transfers, whether on a local network or the internet.",