Compare commits

...

19 Commits

Author SHA1 Message Date
Andras Bacsai
a50317cc76 Merge pull request #1209 from coollabsio/next
v4.0.0-beta.28
2023-09-08 16:28:04 +02:00
Andras Bacsai
8afa98a1ca fix: sentry 4451028626 2023-09-08 16:19:45 +02:00
Andras Bacsai
f6737f21dd feat: developer view for env variables 2023-09-08 16:16:59 +02:00
Andras Bacsai
e4a51cc116 fix: sentry 4459819517 2023-09-08 16:16:28 +02:00
Andras Bacsai
acd78ae196 feat: Telegram topics separation 2023-09-08 14:15:28 +02:00
Andras Bacsai
953bcfb5bb fix: db backup job 2023-09-08 12:23:46 +02:00
Andras Bacsai
dacfab8b29 Update BUG_REPORT.yml 2023-09-08 12:08:16 +02:00
Andras Bacsai
48b3e99939 Merge pull request #1203 from coollabsio/next
v4.0.0-beta.27
2023-09-08 12:06:34 +02:00
Andras Bacsai
41ad67c7c9 updates 2023-09-08 12:05:15 +02:00
Andras Bacsai
b49725cb1c fix: bug 2023-09-08 09:37:58 +02:00
Andras Bacsai
75e674a966 version++ 2023-09-08 09:27:44 +02:00
Andras Bacsai
9d826d9fb4 Merge pull request #1202 from coollabsio/next
v4.0.0-beta.26
2023-09-08 09:08:19 +02:00
Andras Bacsai
0af221f9fc udpate 2023-09-08 09:06:00 +02:00
Andras Bacsai
9066c9bf90 update readme 2023-09-08 09:00:44 +02:00
Andras Bacsai
77e3208f00 feat: public database 2023-09-07 13:23:34 +02:00
Andras Bacsai
db5ecf07bd version++ 2023-09-07 09:14:27 +02:00
Andras Bacsai
7f28aa6985 Merge pull request #1201 from coollabsio/next
v4.0.0-beta.25
2023-09-07 09:04:33 +02:00
Andras Bacsai
9d53e04ce9 fix: saveModel email settings 2023-09-07 08:48:16 +02:00
Andras Bacsai
3db9a1dd6e version++ 2023-09-06 15:26:17 +02:00
30 changed files with 454 additions and 88 deletions

26
.github/ISSUE_TEMPLATE/BUG_REPORT.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Bug report
description: Create a new bug report
title: '[Bug]: '
body:
- type: textarea
attributes:
label: Description
description: A clear and concise description of the problem
validations:
required: true
- type: textarea
attributes:
label: Minimal Reproduction (if possible, example repository)
description: Please provide a step by step guide to reproduce the issue
validations:
required: true
- type: textarea
attributes:
label: Exception or Error
description: Please provide error logs if possible.
- type: input
attributes:
label: Version
description: Coolify's version (see bottom left corner).
validations:
required: true

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: 🤔 Community Support (Chat)
url: https://coollabs.io/discord
about: Reach out to us on Discord.
- name: 🙋‍♂️ Feature Requests
url: https://github.com/coollabsio/coolify/discussions/categories/feature-requests-ideas
about: All feature requests will be discussed here.

1
.gitignore vendored
View File

@@ -31,3 +31,4 @@ _ide_helper.php
_ide_helper_models.php
.rnd
/.ssh
scripts/load-test/*

View File

@@ -1,20 +1,30 @@
# Coolify v4 Beta
# About the Project
An open-source & self-hostable Heroku / Netlify alternative.
Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc.
It helps you to manage your servers, applications, databases on your own hardware, all you need is SSH connection. You can manage VPS, Bare Metal, Raspberry PI's anything.
Image if you could have the ease of a cloud but with your own servers. That is **Coolify**.
No vendor lock-in, which means that all the configuration for your applications/databases/etc are saved to your server. So if you decide to stop using Coolify (oh nooo), you could still manage your running resources. You just lose the automations and all the magic. 🪄️
For more information, take a look at our landing page [here](https://coolify.io).
> If you are looking for previous (v3) version, it is [here](https://github.com/coollabsio/coolify/tree/v3).
# Cloud
If you do not want to self-host Coolify, there is a paid cloud version available: https://app.coolify.io
You can easily attach your own servers, get all the automations, free email notifications, etc.
For more information & pricing, take a look at our landing page [here](https://coolify.io).
# Beta
You are checking the next-gen of Coolify, aka v4. Hi 👋
The latest version (v4) is still in beta. That does not mean it is unstable. All the features that are available are stable enough be usable in real-life.
It is still in beta, lots of improvements will come every day. Things could break, but we are working hard to make it stable as soon as possible. If you find any bugs, please report them.
Automatic updates are available, so you will receive the latest version as soon as it is released.
If you are looking for v3, check out the [v3 branch](https://github.com/coollabsio/coolify/tree/v3).
## What's new?
Well, the whole tech stack changed, core is different, so yeah, a lot (documentation incoming).
There are hundreds of people using it for managing their client's applications, freelancers, hobbyists, businesses.
# Installation
@@ -26,13 +36,19 @@ You can find the installation script [here](./scripts/install.sh).
## Support
- Twitter: [@heyandras](https://twitter.com/heyandras)
- Mastodon: [@andrasbacsai@fosstodon.org](https://fosstodon.org/@andrasbacsai)
- Email: [andras@coollabs.io](mailto:andras@coollabs.io)
- Discord: [Invitation](https://coollabs.io/discord)
- Telegram: [@andrasbacsai](https://t.me/andrasbacsai)
Contact us [here](https://docs.coollabs.io/contact).
---
## Recognitions
<a href="https://news.ycombinator.com/item?id=26624341">
<img
style="width: 250px; height: 54px;" width="250" height="54"
alt="Featured on Hacker News"
src="https://hackernews-badge.vercel.app/api?id=26624341"
/>
</a>
<a href="https://www.producthunt.com/posts/coolify?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-coolify" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=338273&theme=light" alt="Coolify - An&#0032;open&#0045;source&#0032;&#0038;&#0032;self&#0045;hostable&#0032;Heroku&#0044;&#0032;Netlify&#0032;alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
## 💰 Financial Contributors

View File

@@ -107,7 +107,14 @@ class EmailSettings extends Component
return general_error_handler($e, $this);
}
}
public function saveModel()
{
$this->team->save();
if (is_a($this->team, Team::class)) {
refreshSession();
}
$this->emit('success', 'Settings saved.');
}
public function submit()
{
try {

View File

@@ -17,6 +17,10 @@ class TelegramSettings extends Component
'team.telegram_notifications_deployments' => 'nullable|boolean',
'team.telegram_notifications_status_changes' => 'nullable|boolean',
'team.telegram_notifications_database_backups' => 'nullable|boolean',
'team.telegram_notifications_test_message_thread_id' => 'nullable|string',
'team.telegram_notifications_deployments_message_thread_id' => 'nullable|string',
'team.telegram_notifications_status_changes_message_thread_id' => 'nullable|string',
'team.telegram_notifications_database_backups_message_thread_id' => 'nullable|string',
];
protected $validationAttributes = [
'team.telegram_token' => 'Token',

View File

@@ -42,8 +42,13 @@ class Heading extends Component
["docker rm -f {$this->database->uuid}"],
$this->database->destination->server
);
if ($this->database->is_public) {
stopPostgresProxy($this->database);
$this->database->is_public = false;
}
$this->database->status = 'stopped';
$this->database->save();
$this->emit('refresh');
// $this->database->environment->project->team->notify(new StatusChanged($this->database));
}

View File

@@ -12,6 +12,7 @@ class General extends Component
public StandalonePostgresql $database;
public string $new_filename;
public string $new_content;
public string $db_url;
protected $listeners = ['refresh', 'save_init_script', 'delete_init_script'];
@@ -26,6 +27,8 @@ class General extends Component
'database.init_scripts' => 'nullable',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
];
protected $validationAttributes = [
'database.name' => 'Name',
@@ -38,8 +41,44 @@ class General extends Component
'database.init_scripts' => 'Init Scripts',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
];
public function mount()
{
$this->getDbUrl();
}
public function getDbUrl() {
if ($this->database->is_public) {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->destination->server->ip}:{$this->database->public_port}/{$this->database->postgres_db}";
} else {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->uuid}:5432/{$this->database->postgres_db}";
}
}
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
$this->emit('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
$this->emit('success', 'Starting TCP proxy...');
startPostgresProxy($this->database);
$this->emit('success', 'Database is now publicly accessible.');
} else {
stopPostgresProxy($this->database);
$this->emit('success', 'Database is no longer publicly accessible.');
}
$this->getDbUrl();
$this->database->save();
} catch(Exception $e) {
$this->database->is_public = !$this->database->is_public;
return general_error_handler(err: $e, that: $this);
}
}
public function save_init_script($script)
{
$this->database->init_scripts = filter($this->database->init_scripts, fn ($s) => $s['filename'] !== $script['filename']);

View File

@@ -5,21 +5,84 @@ namespace App\Http\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
use Str;
class All extends Component
{
public $resource;
public bool $showPreview = false;
public string|null $modalId = null;
public ?string $variables = null;
public ?string $variablesPreview = null;
public string $view = 'normal';
protected $listeners = ['refreshEnvs', 'submit'];
public function mount()
{
$resourceClass = get_class($this->resource);
$resourceWithPreviews = ['App\Models\Application'];
$simpleDockerfile = !is_null(data_get($this->resource, 'dockerfile'));
if (Str::of($resourceClass)->contains($resourceWithPreviews) && !$simpleDockerfile) {
$this->showPreview = true;
}
$this->modalId = new Cuid2(7);
$this->getDevView();
}
public function getDevView()
{
$this->variables = $this->resource->environment_variables->map(function ($item) {
return "$item->key=$item->value";
})->sort()->join('
');
if ($this->showPreview) {
$this->variablesPreview = $this->resource->environment_variables_preview->map(function ($item) {
return "$item->key=$item->value";
})->sort()->join('
');
}
}
public function switch()
{
$this->view = $this->view === 'normal' ? 'dev' : 'normal';
}
public function saveVariables($isPreview)
{
if ($isPreview) {
$variables = parseEnvFormatToArray($this->variablesPreview);
$existingVariables = $this->resource->environment_variables_preview();
$this->resource->environment_variables_preview()->delete();
} else {
$variables = parseEnvFormatToArray($this->variables);
$existingVariables = $this->resource->environment_variables();
$this->resource->environment_variables()->delete();
}
foreach ($variables as $key => $variable) {
$found = $existingVariables->where('key', $key)->first();
if ($found) {
$found->value = $variable;
$found->save();
continue;
} else {
$environment = new EnvironmentVariable();
$environment->key = $key;
$environment->value = $variable;
$environment->is_build_time = false;
$environment->is_preview = $isPreview ? true : false;
if ($this->resource->type() === 'application') {
$environment->application_id = $this->resource->id;
}
if ($this->resource->type() === 'standalone-postgresql') {
$environment->standalone_postgresql_id = $this->resource->id;
}
$environment->save();
}
}
$this->refreshEnvs();
}
public function refreshEnvs()
{
$this->resource->refresh();
$this->getDevView();
}
public function submit($data)
@@ -43,7 +106,7 @@ class All extends Component
$environment->standalone_postgresql_id = $this->resource->id;
}
$environment->save();
$this->resource->refresh();
$this->refreshEnvs();
$this->emit('success', 'Environment variable added successfully.');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);

View File

@@ -15,7 +15,7 @@ class IsBoardingFlow
*/
public function handle(Request $request, Closure $next): Response
{
ray()->showQueries()->color('orange');
// ray()->showQueries()->color('orange');
if (showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) {
return redirect('boarding');
}

View File

@@ -24,28 +24,32 @@ class DatabaseBackupJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public Team|null $team = null;
public ?Team $team = null;
public Server $server;
public ScheduledDatabaseBackup|null $backup;
public ?ScheduledDatabaseBackup $backup = null;
public string $database_type;
public StandalonePostgresql $database;
public ?StandalonePostgresql $database = null;
public string $database_status;
public string|null $container_name = null;
public ScheduledDatabaseBackupExecution|null $backup_log = null;
public ?string $container_name = null;
public ?ScheduledDatabaseBackupExecution $backup_log = null;
public string $backup_status;
public string|null $backup_location = null;
public ?string $backup_location = null;
public string $backup_dir;
public string $backup_file;
public int $size = 0;
public string|null $backup_output = null;
public S3Storage|null $s3 = null;
public ?string $backup_output = null;
public ?S3Storage $s3 = null;
public function __construct($backup)
{
$this->backup = $backup;
$this->team = Team::find($backup->team_id);
$this->database = $this->backup->database;
if (!$this->database) {
ray('Database not found');
return;
}
$this->database_type = $this->database->type();
$this->server = $this->database->destination->server;
$this->database_status = $this->database->status;

View File

@@ -31,6 +31,7 @@ class SendMessageToTelegramJob implements ShouldQueue
public array $buttons,
public string $token,
public string $chatId,
public ?string $topicId = null,
) {
}
@@ -63,7 +64,9 @@ class SendMessageToTelegramJob implements ShouldQueue
'chat_id' => $this->chatId,
'text' => $this->text,
];
ray($payload);
if ($this->topicId) {
$payload['message_thread_id'] = $this->topicId;
}
$response = Http::post($url, $payload);
if ($response->failed()) {
throw new \Exception('Telegram notification failed with ' . $response->status() . ' status code.' . $response->body());

View File

@@ -105,7 +105,7 @@ class Application extends BaseModel
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false);
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->orderBy('key', 'asc');
}
public function runtime_environment_variables(): HasMany
@@ -127,7 +127,7 @@ class Application extends BaseModel
public function environment_variables_preview(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true);
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderBy('key', 'asc');
}
public function runtime_environment_variables_preview(): HasMany

View File

@@ -20,13 +20,16 @@ class EnvironmentVariable extends Model
{
static::created(function ($environment_variable) {
if ($environment_variable->application_id && !$environment_variable->is_preview) {
ModelsEnvironmentVariable::create([
'key' => $environment_variable->key,
'value' => $environment_variable->value,
'is_build_time' => $environment_variable->is_build_time,
'application_id' => $environment_variable->application_id,
'is_preview' => true,
]);
$found = ModelsEnvironmentVariable::where('key', $environment_variable->key)->where('application_id', $environment_variable->application_id)->where('is_preview',true)->first();
if (!$found) {
ModelsEnvironmentVariable::create([
'key' => $environment_variable->key,
'value' => $environment_variable->value,
'is_build_time' => $environment_variable->is_build_time,
'application_id' => $environment_variable->application_id,
'is_preview' => true,
]);
}
}
});
}

View File

@@ -23,6 +23,6 @@ class StandaloneDocker extends BaseModel
public function attachedTo()
{
return $this->applications->count() > 0 || $this->databases->count() > 0;
return $this->applications?->count() > 0 || $this->databases?->count() > 0;
}
}

View File

@@ -28,7 +28,7 @@ class Team extends Model implements SendsDiscord, SendsEmail
{
return [
"token" => data_get($this, 'telegram_token', null),
"chat_id" => data_get($this, 'telegram_chat_id', null)
"chat_id" => data_get($this, 'telegram_chat_id', null),
];
}

View File

@@ -10,16 +10,30 @@ class TelegramChannel
{
$data = $notification->toTelegram($notifiable);
$telegramData = $notifiable->routeNotificationForTelegram();
$message = data_get($data, 'message');
$buttons = data_get($data, 'buttons', []);
ray($message, $buttons);
$telegramToken = data_get($telegramData, 'token');
$chatId = data_get($telegramData, 'chat_id');
$topicId = null;
$topicsInstance = get_class($notification);
if (!$telegramToken || !$chatId || !$message) {
throw new \Exception('Telegram token, chat id and message are required');
switch ($topicsInstance) {
case 'App\Notifications\StatusChange':
$topicId = data_get($notifiable, 'telegram_notifications_status_changes_message_thread_id');
break;
case 'App\Notifications\Test':
$topicId = data_get($notifiable, 'telegram_notifications_test_message_thread_id');
break;
case 'App\Notifications\Deployment':
$topicId = data_get($notifiable, 'telegram_notifications_deployments_message_thread_id');
break;
case 'App\Notifications\DatabaseBackup':
$topicId = data_get($notifiable, 'telegram_notifications_database_backups_message_thread_id');
break;
}
dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId));
if (!$telegramToken || !$chatId || !$message) {
return;
}
dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId, $topicId));
}
}

View File

@@ -45,7 +45,7 @@ class Test extends Notification implements ShouldQueue
"buttons" => [
[
"text" => "Go to your dashboard",
"url" => 'https://coolify.io'
"url" => base_url()
]
],
];

View File

@@ -20,7 +20,7 @@ function create_standalone_postgresql($environment_id, $destination_uuid): Stand
}
return StandalonePostgresql::create([
'name' => generate_database_name('postgresql'),
'postgres_password' => \Illuminate\Support\Str::password(),
'postgres_password' => \Illuminate\Support\Str::password(symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),

View File

@@ -1,6 +1,7 @@
<?php
use App\Models\Server;
use App\Models\StandalonePostgresql;
use Symfony\Component\Yaml\Yaml;
function get_proxy_path()
@@ -166,3 +167,75 @@ function setup_default_redirect_404(string|null $redirect_url, Server $server)
}
}
}
function startPostgresProxy(StandalonePostgresql $database)
{
$containerName = "{$database->uuid}-proxy";
$configuration_dir = database_proxy_dir($database->uuid);
$nginxconf = <<<EOF
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
events {
worker_connections 1024;
}
stream {
server {
listen $database->public_port;
proxy_pass $database->uuid:5432;
}
}
EOF;
$docker_compose = [
'version' => '3.8',
'services' => [
$containerName => [
'image' => "nginx:stable-alpine",
'container_name' => $containerName,
'restart' => RESTART_MODE,
'volumes' => [
"$configuration_dir/nginx.conf:/etc/nginx/nginx.conf:ro",
],
'ports' => [
"$database->public_port:$database->public_port",
],
'networks' => [
$database->destination->network,
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
'stat /etc/nginx/nginx.conf || exit 1',
],
'interval' => '5s',
'timeout' => '5s',
'retries' => 3,
'start_period' => '1s'
],
]
],
'networks' => [
$database->destination->network => [
'external' => true,
'name' => $database->destination->network,
'attachable' => true,
]
]
];
$dockercompose_base64 = base64_encode(Yaml::dump($docker_compose, 4, 2));
$nginxconf_base64 = base64_encode($nginxconf);
instant_remote_process([
"mkdir -p $configuration_dir",
"echo '{$nginxconf_base64}' | base64 -d > $configuration_dir/nginx.conf",
"echo '{$dockercompose_base64}' | base64 -d > $configuration_dir/docker-compose.yaml",
"docker compose --project-directory {$configuration_dir} up -d >/dev/null",
], $database->destination->server);
}
function stopPostgresProxy(StandalonePostgresql $database)
{
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server);
}

View File

@@ -28,6 +28,10 @@ function database_configuration_dir(): string
{
return '/data/coolify/databases';
}
function database_proxy_dir($uuid): string
{
return "/data/coolify/databases/$uuid/proxy";
}
function backup_dir(): string
{
@@ -292,3 +296,25 @@ function setNotificationChannels($notifiable, $event)
}
return $channels;
}
function parseEnvFormatToArray($env_file_contents) {
$env_array = array();
$lines = explode("\n", $env_file_contents);
foreach ($lines as $line) {
if ($line === '' || substr($line, 0, 1) === '#') {
continue;
}
$equals_pos = strpos($line, '=');
if ($equals_pos !== false) {
$key = substr($line, 0, $equals_pos);
$value = substr($line, $equals_pos + 1);
if (substr($value, 0, 1) === '"' && substr($value, -1) === '"') {
$value = substr($value, 1, -1);
}
elseif (substr($value, 0, 1) === "'" && substr($value, -1) === "'") {
$value = substr($value, 1, -1);
}
$env_array[$key] = $value;
}
}
return $env_array;
}

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.24',
'release' => '4.0.0-beta.28',
'server_name' => env('APP_ID', 'coolify'),
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.24';
return '4.0.0-beta.28';

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
{
public function up(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->text('telegram_notifications_test_message_thread_id')->nullable();
$table->text('telegram_notifications_deployments_message_thread_id')->nullable();
$table->text('telegram_notifications_status_changes_message_thread_id')->nullable();
$table->text('telegram_notifications_database_backups_message_thread_id')->nullable();
});
}
public function down(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->dropColumn('telegram_message_thread_id');
$table->dropColumn('telegram_notifications_test_message_thread_id');
$table->dropColumn('telegram_notifications_deployments_message_thread_id');
$table->dropColumn('telegram_notifications_status_changes_message_thread_id');
$table->dropColumn('telegram_notifications_database_backups_message_thread_id');
});
}
};

View File

@@ -24,7 +24,7 @@
@endif
</span>
<input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }}
@if ($instantSave) wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
@if ($instantSave) wire:loading.attr="disabled" wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
wire:model.defer={{ $id }} @else wire:model.defer={{ $value ?? $id }} @endif />
</label>
</div>

View File

@@ -16,27 +16,50 @@
<x-forms.checkbox instantSave id="team.telegram_enabled" label="Notification Enabled" />
</div>
<div class="flex gap-2">
<x-forms.input type="password" helper="Get it from the <a class='inline-block text-white underline' href='https://t.me/botfather' target='_blank'>BotFather Bot</a> on Telegram." required
id="team.telegram_token" label="Token" />
<x-forms.input type="password" helper="Recommended to add your bot to a group chat and add its Chat ID here." required
<x-forms.input type="password"
helper="Get it from the <a class='inline-block text-white underline' href='https://t.me/botfather' target='_blank'>BotFather Bot</a> on Telegram."
required id="team.telegram_token" label="Token" />
<x-forms.input
helper="Recommended to add your bot to a group chat and add its Chat ID here." required
id="team.telegram_chat_id" label="Chat ID" />
</div>
</form>
@if (data_get($team, 'telegram_enabled'))
<h3 class="mt-4">Subscribe to events</h3>
<div class="w-64">
<h2 class="mt-4">Subscribe to events</h2>
<div class="w-96">
@if (isDev())
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_test" label="Test" />
<h3 class="mt-4">Test</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_test" label="Enabled" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_test_message_thread_id" label="Custom Topic ID" />
</div>
@endif
<h4 class="mt-4">General</h4>
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_status_changes"
label="Container Status Changes" />
<h4 class="mt-4">Applications</h4>
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_deployments"
label="Deployments" />
<h4 class="mt-4">Databases</h4>
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_database_backups"
label="Backup Statuses" />
<h3 class="mt-4">Container Status Changes</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_status_changes"
label="Enabled" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_status_changes_message_thread_id" label="Custom Topic ID" />
</div>
<h3 class="mt-4">Application Deployments</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_deployments"
label="Enabled" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_deployments_message_thread_id" label="Custom Topic ID" />
</div>
<h3 class="mt-4">Backup Status</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_database_backups"
label="Enabled" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_database_backups_message_thread_id" label="Custom Topic ID" />
</div>
</div>
@endif
@endif
</form>
</div>

View File

@@ -51,10 +51,15 @@
<x-forms.input label="Host Auth Method" id="database.postgres_host_auth_method"
placeholder="If empty, use default. See in docker docs." />
</div>
<div class="">
<div class="flex flex-col gap-2">
<h3 class="py-2">Network</h3>
<x-forms.input placeholder="3000:5432" id="database.ports_mappings" label="Ports Mappings"
<div class="flex items-end gap-2">
<x-forms.input placeholder="3000:5432" id="database.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold text-warning'>Example</span>3000:5432,3002:5433" />
<x-forms.input placeholder="5432" disabled="{{$database->is_public}}" id="database.public_port" label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div>
<x-forms.input label="Postgres URL" readonly wire:model="db_url" />
</div>
</form>
<div class="pb-16">

View File

@@ -4,23 +4,40 @@
<h2>Environment Variables</h2>
<x-forms.button class="btn" onclick="newVariable.showModal()">+ Add</x-forms.button>
<livewire:project.shared.environment-variable.add />
<x-forms.button
wire:click='switch'>{{ $view === 'normal' ? 'Developer view' : 'Normal view' }}</x-forms.button>
</div>
<div>Environment (secrets) variables for this resource.</div>
<div>Environment variables (secrets) for this resource.</div>
</div>
@forelse ($resource->environment_variables as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" />
@empty
<div class="text-neutral-500">No environment variables found.</div>
@endforelse
@if ($resource->type() === 'application' && $resource->environment_variables_preview->count() > 0)
<div>
<h3>Preview Deployments</h3>
<div>Environment (secrets) variables for Preview Deployments.</div>
</div>
@foreach ($resource->environment_variables_preview as $env)
@if ($view === 'normal')
@forelse ($resource->environment_variables as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" />
@endforeach
@empty
<div class="text-neutral-500">No environment variables found.</div>
@endforelse
@if ($resource->type() === 'application' && $resource->environment_variables_preview->count() > 0 && $showPreview)
<div>
<h3>Preview Deployments</h3>
<div>Environment (secrets) variables for Preview Deployments.</div>
</div>
@foreach ($resource->environment_variables_preview as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" />
@endforeach
@endif
@else
<form wire:submit.prevent='saveVariables(false)' class="flex flex-col gap-2">
<x-forms.textarea rows=5 class="whitespace-pre-wrap" label="Environment Variables"
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"
id="variablesPreview"></x-forms.textarea>
<x-forms.button type="submit" class="btn btn-primary">Save</x-forms.button>
</form>
@endif
@endif
</div>

View File

@@ -22,7 +22,7 @@
class="relative flex duration-300 transform transition ease-in-out max-w-md w-full pointer-events-auto {{ $position->is('center') ? 'text-center' : 'text-left' }}"
:class="toast.select({ error: 'text-white', info: 'text-white', success: 'text-white', warning: 'text-white' })"
>
<i class=" flex items-center gap-2 select-none not-italic pr-6 pl-4 py-3 rounded shadow-lg w-full {{ $alignment->is('bottom') ? 'mt-3' : 'mb-3' }}"
<i class=" flex items-center gap-2 select-none not-italic pr-6 pl-4 py-3 rounded shadow-lg w-full {{ $alignment->is('bottom') ? 'mt-3' : 'mb-3' }}"
:class="toast.select({
error: 'bg-coolgray-300',
info: 'bg-coolgray-300',
@@ -71,7 +71,7 @@
</svg>
</div>
</template>
<span x-html="toast.message" />
<span x-html="toast.message" class="w-full pr-10 break-words" />
</i>
@if ($closeable)

View File

@@ -4,7 +4,7 @@
"version": "3.12.36"
},
"v4": {
"version": "4.0.0-beta.24"
"version": "4.0.0-beta.28"
}
}
}