This commit is contained in:
Andras Bacsai 2023-06-05 12:07:55 +02:00
parent 0f28acac00
commit e5aad4d170
42 changed files with 518 additions and 238 deletions

View File

@ -21,7 +21,7 @@ class Deploy extends Component
protected $source; protected $source;
protected $listeners = [ protected $listeners = [
'applicationStatusChanged' => 'applicationStatusChanged', 'applicationStatusChanged',
]; ];
public function mount() public function mount()
@ -40,8 +40,12 @@ protected function set_deployment_uuid()
$this->deployment_uuid = new Cuid2(7); $this->deployment_uuid = new Cuid2(7);
$this->parameters['deployment_uuid'] = $this->deployment_uuid; $this->parameters['deployment_uuid'] = $this->deployment_uuid;
} }
public function deploy(bool $force = false) public function deploy(bool $force = false, bool|null $debug = null)
{ {
if ($debug && !$this->application->settings->is_debug_enabled) {
$this->application->settings->is_debug_enabled = true;
$this->application->settings->save();
}
$this->set_deployment_uuid(); $this->set_deployment_uuid();
queue_application_deployment( queue_application_deployment(
@ -62,5 +66,6 @@ public function stop()
instant_remote_process(["docker rm -f {$this->application->uuid}"], $this->application->destination->server); instant_remote_process(["docker rm -f {$this->application->uuid}"], $this->application->destination->server);
$this->application->status = get_container_status(server: $this->application->destination->server, container_id: $this->application->uuid); $this->application->status = get_container_status(server: $this->application->destination->server, container_id: $this->application->uuid);
$this->application->save(); $this->application->save();
$this->emit('applicationStatusChanged');
} }
} }

View File

@ -2,15 +2,12 @@
namespace App\Http\Livewire\Project\Application\EnvironmentVariable; namespace App\Http\Livewire\Project\Application\EnvironmentVariable;
use App\Models\Application;
use App\Models\EnvironmentVariable;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\Route;
use Livewire\Component; use Livewire\Component;
class Add extends Component class Add extends Component
{ {
public $parameters; public $parameters;
public bool $is_preview = false;
public string $key; public string $key;
public string $value; public string $value;
public bool $is_build_time = false; public bool $is_build_time = false;
@ -32,6 +29,7 @@ public function submit()
'key' => $this->key, 'key' => $this->key,
'value' => $this->value, 'value' => $this->value,
'is_build_time' => $this->is_build_time, 'is_build_time' => $this->is_build_time,
'is_preview' => $this->is_preview,
]); ]);
} }
public function clear() public function clear()

View File

@ -21,6 +21,7 @@ public function submit($data)
'key' => $data['key'], 'key' => $data['key'],
'value' => $data['value'], 'value' => $data['value'],
'is_build_time' => $data['is_build_time'], 'is_build_time' => $data['is_build_time'],
'is_preview' => $data['is_preview'],
'application_id' => $this->application->id, 'application_id' => $this->application->id,
]); ]);
$this->application->refresh(); $this->application->refresh();

View File

@ -11,7 +11,7 @@ class Status extends Component
public function applicationStatusChanged() public function applicationStatusChanged()
{ {
$this->emit('applicationStatusChanged');
$this->application->refresh(); $this->application->refresh();
$this->emit('applicationStatusChanged');
} }
} }

View File

@ -14,16 +14,22 @@
class PublicGitRepository extends Component class PublicGitRepository extends Component
{ {
public string $repository_url; public string $repository_url;
private object $repository_url_parsed;
public int $port = 3000; public int $port = 3000;
public string $type; public string $type;
public $parameters; public $parameters;
public $query; public $query;
public $github_apps; public $branches = [];
public $gitlab_apps; public string $selected_branch = 'main';
public bool $is_static = false; public bool $is_static = false;
public null|string $publish_directory = null; public string|null $publish_directory = null;
private GithubApp|GitlabApp $git_source;
private string $git_host;
private string $git_repository;
private string $git_branch;
protected $rules = [ protected $rules = [
'repository_url' => 'required|url', 'repository_url' => 'required|url',
@ -34,7 +40,7 @@ class PublicGitRepository extends Component
public function mount() public function mount()
{ {
if (config('app.env') === 'local') { if (config('app.env') === 'local') {
$this->repository_url = 'https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify'; $this->repository_url = 'https://github.com/coollabsio/coolify-examples';
$this->port = 3000; $this->port = 3000;
} }
$this->parameters = get_parameters(); $this->parameters = get_parameters();
@ -52,18 +58,43 @@ public function instantSave()
} }
$this->emit('saved', 'Application settings updated!'); $this->emit('saved', 'Application settings updated!');
} }
public function load_branches()
{
$this->get_git_source();
try {
['data' => $data] = get_from_git_api($this->git_source, "/repos/{$this->git_repository}/branches");
$this->branches = collect($data)->pluck('name')->toArray();
} catch (\Throwable $th) {
return general_error_handler($th, $this);
}
}
private function get_git_source()
{
$this->repository_url_parsed = Url::fromString($this->repository_url);
$this->git_host = $this->repository_url_parsed->getHost();
$this->git_repository = $this->repository_url_parsed->getSegment(1) . '/' . $this->repository_url_parsed->getSegment(2);
$this->git_branch = $this->repository_url_parsed->getSegment(4) ?? 'main';
if ($this->git_host == 'github.com') {
$this->git_source = GithubApp::where('name', 'Public GitHub')->first();
} elseif ($this->git_host == 'gitlab.com') {
$this->git_source = GitlabApp::where('name', 'Public GitLab')->first();
} elseif ($this->git_host == 'bitbucket.org') {
// Not supported yet
}
}
public function submit() public function submit()
{ {
try { try {
$this->validate(); $this->validate();
$url = Url::fromString($this->repository_url);
$git_host = $url->getHost();
$git_repository = $url->getSegment(1) . '/' . $url->getSegment(2);
$git_branch = $url->getSegment(4) ?? 'main';
$destination_uuid = $this->query['destination']; $destination_uuid = $this->query['destination'];
$project_uuid = $this->parameters['project_uuid'];
$environment_name = $this->parameters['environment_name'];
$this->get_git_source();
$this->git_branch = $this->selected_branch ?? $this->git_branch;
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); $destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (!$destination) { if (!$destination) {
$destination = SwarmDocker::where('uuid', $destination_uuid)->first(); $destination = SwarmDocker::where('uuid', $destination_uuid)->first();
@ -73,29 +104,24 @@ public function submit()
} }
$destination_class = $destination->getMorphClass(); $destination_class = $destination->getMorphClass();
$project = Project::where('uuid', $this->parameters['project_uuid'])->first(); $project = Project::where('uuid', $project_uuid)->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); $environment = $project->load(['environments'])->environments->where('name', $environment_name)->first();
$application_init = [ $application_init = [
'name' => generate_application_name($git_repository, $git_branch), 'name' => generate_application_name($this->git_repository, $this->git_branch),
'git_repository' => $git_repository, 'git_repository' => $this->git_repository,
'git_branch' => $git_branch, 'git_branch' => $this->git_branch,
'build_pack' => 'nixpacks', 'build_pack' => 'nixpacks',
'ports_exposes' => $this->port, 'ports_exposes' => $this->port,
'publish_directory' => $this->publish_directory, 'publish_directory' => $this->publish_directory,
'environment_id' => $environment->id, 'environment_id' => $environment->id,
'destination_id' => $destination->id, 'destination_id' => $destination->id,
'destination_type' => $destination_class, 'destination_type' => $destination_class,
'source_id' => $this->git_source->id,
'source_type' => $this->git_source->getMorphClass()
]; ];
if ($git_host == 'github.com') {
$application_init['source_id'] = GithubApp::where('name', 'Public GitHub')->first()->id;
$application_init['source_type'] = GithubApp::class;
} elseif ($git_host == 'gitlab.com') {
$application_init['source_id'] = GitlabApp::where('name', 'Public GitLab')->first()->id;
$application_init['source_type'] = GitlabApp::class;
} elseif ($git_host == 'bitbucket.org') {
}
$application = Application::create($application_init); $application = Application::create($application_init);
$application->settings->is_static = $this->is_static; $application->settings->is_static = $this->is_static;
$application->settings->save(); $application->settings->save();
@ -106,7 +132,7 @@ public function submit()
'application_uuid' => $application->uuid, 'application_uuid' => $application->uuid,
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
return general_error_handler($e); return general_error_handler($e, $this);
} }
} }
} }

View File

@ -63,7 +63,7 @@ public function __construct(
$this->application = Application::find($this->application_id); $this->application = Application::find($this->application_id);
if ($this->pull_request_id) { if ($this->pull_request_id !== 0) {
$this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id); $this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id);
} }
@ -97,7 +97,7 @@ public function handle(): void
} }
$this->workdir = "/artifacts/{$this->deployment_uuid}"; $this->workdir = "/artifacts/{$this->deployment_uuid}";
if ($this->pull_request_id) { if ($this->pull_request_id !== 0) {
ray('Deploying pull/' . $this->pull_request_id . '/head for application: ' . $this->application->name); ray('Deploying pull/' . $this->pull_request_id . '/head for application: ' . $this->application->name);
$this->deploy_pull_request(); $this->deploy_pull_request();
} else { } else {
@ -177,7 +177,7 @@ private function build_image()
"echo -n 'Building image... '", "echo -n 'Building image... '",
]); ]);
if ($this->application->settings->is_static) { if ($this->application->settings->is_static && isset($this->application->build_command)) {
$this->execute_now([ $this->execute_now([
$this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t { $this->build_image_name {$this->workdir}"), $this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t { $this->build_image_name {$this->workdir}"),
], isDebuggable: true); ], isDebuggable: true);
@ -292,10 +292,18 @@ private function execute_in_builder(string $command)
private function generate_environment_variables($ports) private function generate_environment_variables($ports)
{ {
$environment_variables = collect(); $environment_variables = collect();
ray('Generate Environment Variables');
if ($this->pull_request_id === 0) {
ray($this->application->runtime_environment_variables);
foreach ($this->application->runtime_environment_variables as $env) { foreach ($this->application->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value"); $environment_variables->push("$env->key=$env->value");
} }
} else {
ray($this->application->runtime_environment_variables_preview);
foreach ($this->application->runtime_environment_variables_preview as $env) {
$environment_variables->push("$env->key=$env->value");
}
}
// Add PORT if not exists, use the first port as default // Add PORT if not exists, use the first port as default
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) { if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) {
$environment_variables->push("PORT={$ports[0]}"); $environment_variables->push("PORT={$ports[0]}");
@ -305,17 +313,31 @@ private function generate_environment_variables($ports)
private function generate_env_variables() private function generate_env_variables()
{ {
$this->env_args = collect([]); $this->env_args = collect([]);
if ($this->pull_request_id === 0) {
foreach ($this->application->nixpacks_environment_variables as $env) { foreach ($this->application->nixpacks_environment_variables as $env) {
$this->env_args->push("--env {$env->key}={$env->value}"); $this->env_args->push("--env {$env->key}={$env->value}");
} }
} else {
foreach ($this->application->nixpacks_environment_variables_preview as $env) {
$this->env_args->push("--env {$env->key}={$env->value}");
}
}
$this->env_args = $this->env_args->implode(' '); $this->env_args = $this->env_args->implode(' ');
} }
private function generate_build_env_variables() private function generate_build_env_variables()
{ {
$this->build_args = collect(["--build-arg SOURCE_COMMIT={$this->git_commit}"]); $this->build_args = collect(["--build-arg SOURCE_COMMIT={$this->git_commit}"]);
if ($this->pull_request_id === 0) {
foreach ($this->application->build_environment_variables as $env) { foreach ($this->application->build_environment_variables as $env) {
$this->build_args->push("--build-arg {$env->key}={$env->value}"); $this->build_args->push("--build-arg {$env->key}={$env->value}");
} }
} else {
foreach ($this->application->build_environment_variables_preview as $env) {
$this->build_args->push("--build-arg {$env->key}={$env->value}");
}
}
$this->build_args = $this->build_args->implode(' '); $this->build_args = $this->build_args->implode(' ');
} }
private function add_build_env_variables_to_dockerfile() private function add_build_env_variables_to_dockerfile()
@ -336,15 +358,11 @@ private function add_build_env_variables_to_dockerfile()
private function generate_docker_compose() private function generate_docker_compose()
{ {
$ports = $this->application->settings->is_static ? [80] : $this->application->ports_exposes_array; $ports = $this->application->settings->is_static ? [80] : $this->application->ports_exposes_array;
if ($this->pull_request_id) {
$persistent_storages = [];
$volume_names = [];
$environment_variables = [];
} else {
$persistent_storages = $this->generate_local_persistent_volumes(); $persistent_storages = $this->generate_local_persistent_volumes();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names(); $volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables($ports); $environment_variables = $this->generate_environment_variables($ports);
}
$docker_compose = [ $docker_compose = [
'version' => '3.8', 'version' => '3.8',
'services' => [ 'services' => [
@ -385,7 +403,7 @@ private function generate_docker_compose()
] ]
] ]
]; ];
if (count($this->application->ports_mappings_array) > 0 && !$this->pull_request_id) { if (count($this->application->ports_mappings_array) > 0 && $this->pull_request_id === 0) {
$docker_compose['services'][$this->container_name]['ports'] = $this->application->ports_mappings_array; $docker_compose['services'][$this->container_name]['ports'] = $this->application->ports_mappings_array;
} }
if (count($persistent_storages) > 0) { if (count($persistent_storages) > 0) {
@ -400,8 +418,12 @@ private function generate_local_persistent_volumes()
{ {
foreach ($this->application->persistentStorages as $persistentStorage) { foreach ($this->application->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name; $volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
if ($this->pull_request_id !== 0) {
$volume_name = $volume_name . '-pr-' . $this->pull_request_id;
}
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path; $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
} }
ray('local_persistent_volumes', $local_persistent_volumes);
return $local_persistent_volumes ?? []; return $local_persistent_volumes ?? [];
} }
@ -411,8 +433,14 @@ private function generate_local_persistent_volumes_only_volume_names()
if ($persistentStorage->host_path) { if ($persistentStorage->host_path) {
continue; continue;
} }
$local_persistent_volumes_names[$persistentStorage->name] = [ $name = $persistentStorage->name;
'name' => $persistentStorage->name,
if ($this->pull_request_id !== 0) {
$name = $name . '-pr-' . $this->pull_request_id;
}
$local_persistent_volumes_names[$name] = [
'name' => $name,
'external' => false, 'external' => false,
]; ];
} }
@ -443,11 +471,11 @@ private function set_labels_for_applications()
$labels[] = 'coolify.applicationId=' . $this->application->id; $labels[] = 'coolify.applicationId=' . $this->application->id;
$labels[] = 'coolify.type=application'; $labels[] = 'coolify.type=application';
$labels[] = 'coolify.name=' . $this->application->name; $labels[] = 'coolify.name=' . $this->application->name;
if ($this->pull_request_id) { if ($this->pull_request_id !== 0) {
$labels[] = 'coolify.pullRequestId=' . $this->pull_request_id; $labels[] = 'coolify.pullRequestId=' . $this->pull_request_id;
} }
if ($this->application->fqdn) { if ($this->application->fqdn) {
if ($this->pull_request_id) { if ($this->pull_request_id !== 0) {
$preview_fqdn = data_get($this->preview, 'fqdn'); $preview_fqdn = data_get($this->preview, 'fqdn');
$template = $this->application->preview_url_template; $template = $this->application->preview_url_template;
$url = Url::fromString($this->application->fqdn); $url = Url::fromString($this->application->fqdn);
@ -566,7 +594,7 @@ private function set_git_import_settings($git_clone_command)
private function importing_git_repository() private function importing_git_repository()
{ {
$git_clone_command = "git clone -q -b {$this->application->git_branch}"; $git_clone_command = "git clone -q -b {$this->application->git_branch}";
if ($this->pull_request_id) { if ($this->pull_request_id !== 0) {
$pr_branch_name = "pr-{$this->pull_request_id}-coolify"; $pr_branch_name = "pr-{$this->pull_request_id}-coolify";
} }
@ -583,7 +611,7 @@ private function importing_git_repository()
$commands = [$this->execute_in_builder($git_clone_command)]; $commands = [$this->execute_in_builder($git_clone_command)];
if ($this->pull_request_id) { if ($this->pull_request_id !== 0) {
$commands[] = $this->execute_in_builder("cd {$this->workdir} && git fetch origin pull/{$this->pull_request_id}/head:$pr_branch_name >/dev/null 2>&1 && git checkout $pr_branch_name >/dev/null 2>&1"); $commands[] = $this->execute_in_builder("cd {$this->workdir} && git fetch origin pull/{$this->pull_request_id}/head:$pr_branch_name >/dev/null 2>&1 && git checkout $pr_branch_name >/dev/null 2>&1");
} }
return $commands; return $commands;
@ -592,7 +620,7 @@ private function importing_git_repository()
$commands = [ $commands = [
$this->execute_in_builder("git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->application->git_repository}.git {$this->workdir}") $this->execute_in_builder("git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->application->git_repository}.git {$this->workdir}")
]; ];
if ($this->pull_request_id) { if ($this->pull_request_id !== 0) {
$commands[] = $this->execute_in_builder("cd {$this->workdir} && git fetch origin pull/{$this->pull_request_id}/head:$pr_branch_name && git checkout $pr_branch_name"); $commands[] = $this->execute_in_builder("cd {$this->workdir} && git fetch origin pull/{$this->pull_request_id}/head:$pr_branch_name && git checkout $pr_branch_name");
} }
return $commands; return $commands;

View File

@ -99,21 +99,39 @@ public function portsExposesArray(): Attribute
: explode(',', $this->ports_exposes) : explode(',', $this->ports_exposes)
); );
} }
// Normal Deployments
public function environment_variables(): HasMany public function environment_variables(): HasMany
{ {
return $this->hasMany(EnvironmentVariable::class); return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false);
} }
public function runtime_environment_variables(): HasMany public function runtime_environment_variables(): HasMany
{ {
return $this->hasMany(EnvironmentVariable::class)->where('key', 'not like', 'NIXPACKS_%'); return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->where('key', 'not like', 'NIXPACKS_%');
} }
public function build_environment_variables(): HasMany public function build_environment_variables(): HasMany
{ {
return $this->hasMany(EnvironmentVariable::class)->where('is_build_time', true)->where('key', 'not like', 'NIXPACKS_%'); return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->where('is_build_time', true)->where('key', 'not like', 'NIXPACKS_%');
} }
public function nixpacks_environment_variables(): HasMany public function nixpacks_environment_variables(): HasMany
{ {
return $this->hasMany(EnvironmentVariable::class)->where('key', 'like', 'NIXPACKS_%'); return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->where('key', 'like', 'NIXPACKS_%');
}
// Preview Deployments
public function environment_variables_preview(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true);
}
public function runtime_environment_variables_preview(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->where('key', 'not like', 'NIXPACKS_%');
}
public function build_environment_variables_preview(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->where('is_build_time', true)->where('key', 'not like', 'NIXPACKS_%');
}
public function nixpacks_environment_variables_preview(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->where('key', 'like', 'NIXPACKS_%');
} }
public function private_key() public function private_key()
{ {

View File

@ -2,13 +2,28 @@
namespace App\Models; namespace App\Models;
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class EnvironmentVariable extends Model class EnvironmentVariable extends Model
{ {
protected $fillable = ['key', 'value', 'is_build_time', 'application_id']; protected static function booted()
{
static::created(function ($environment_variable) {
if (!$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,
]);
}
});
}
protected $fillable = ['key', 'value', 'is_build_time', 'application_id', 'is_preview'];
protected $casts = [ protected $casts = [
"key" => 'string', "key" => 'string',
'value' => 'encrypted', 'value' => 'encrypted',

View File

@ -17,14 +17,15 @@ public function up(): void
$table->string('key'); $table->string('key');
$table->string('value')->nullable(); $table->string('value')->nullable();
$table->boolean('is_build_time')->default(false); $table->boolean('is_build_time')->default(false);
$table->boolean('is_preview')->default(false);
$table->foreignId('application_id')->nullable(); $table->foreignId('application_id')->nullable();
$table->foreignId('service_id')->nullable(); $table->foreignId('service_id')->nullable();
$table->foreignId('database_id')->nullable(); $table->foreignId('database_id')->nullable();
$table->unique(['key', 'application_id', 'is_build_time']); $table->unique(['key', 'application_id', 'is_build_time', 'is_preview']);
$table->unique(['key', 'service_id', 'is_build_time']); $table->unique(['key', 'service_id', 'is_build_time', 'is_preview']);
$table->unique(['key', 'database_id', 'is_build_time']); $table->unique(['key', 'database_id', 'is_build_time', 'is_preview']);
$table->timestamps(); $table->timestamps();
}); });
} }

8
package-lock.json generated
View File

@ -7,7 +7,7 @@
"dependencies": { "dependencies": {
"@tailwindcss/typography": "0.5.9", "@tailwindcss/typography": "0.5.9",
"alpinejs": "3.12.2", "alpinejs": "3.12.2",
"daisyui": "3.0.0", "daisyui": "3.0.3",
"tailwindcss-scrollbar": "0.1.0" "tailwindcss-scrollbar": "0.1.0"
}, },
"devDependencies": { "devDependencies": {
@ -941,9 +941,9 @@
"dev": true "dev": true
}, },
"node_modules/daisyui": { "node_modules/daisyui": {
"version": "3.0.0", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-3.0.0.tgz", "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-3.0.3.tgz",
"integrity": "sha512-EuNK9JQd5yrPLDynAPQkG/29vjZXFWhBK4HXvM83d9oJU0EmF35UNLLs0cslBFfLK4b+bOuhgoYPJ4BjytOxNQ==", "integrity": "sha512-RSbXsEBj2LonvjOKEI0I64F5xFJrFrthPgxRNeAZKmACQ3NoIoP45lO6UXLW3bm8PVOUGpKf1Br2SWwc1NqnHQ==",
"dependencies": { "dependencies": {
"colord": "^2.9", "colord": "^2.9",
"css-selector-tokenizer": "^0.8", "css-selector-tokenizer": "^0.8",

View File

@ -17,7 +17,7 @@
"dependencies": { "dependencies": {
"@tailwindcss/typography": "0.5.9", "@tailwindcss/typography": "0.5.9",
"alpinejs": "3.12.2", "alpinejs": "3.12.2",
"daisyui": "3.0.0", "daisyui": "3.0.3",
"tailwindcss-scrollbar": "0.1.0" "tailwindcss-scrollbar": "0.1.0"
} }
} }

View File

@ -14,14 +14,14 @@ body {
@apply scrollbar min-h-screen bg-coolgray-100 text-neutral-400 antialiased ; @apply scrollbar min-h-screen bg-coolgray-100 text-neutral-400 antialiased ;
} }
main { main {
@apply px-32 xl:px-14 mx-auto max-w-screen-xl; @apply px-32 xl:px-14 mx-auto max-w-screen-xl pt-10;
} }
input[type="checkbox"] { input[type="checkbox"] {
@apply toggle toggle-warning toggle-xs rounded; @apply toggle toggle-warning toggle-xs rounded;
} }
input { input {
@apply input input-sm placeholder:text-neutral-700 text-white rounded-none; @apply input input-sm h-7 outline-none placeholder:text-neutral-700 text-white rounded-none;
} }
input[type="text"],[type="number"],[type="email"],[type="password"] { input[type="text"],[type="number"],[type="email"],[type="password"] {
@apply read-only:opacity-40; @apply read-only:opacity-40;
@ -34,7 +34,7 @@ textarea {
@apply textarea placeholder:text-neutral-700 text-white rounded-none; @apply textarea placeholder:text-neutral-700 text-white rounded-none;
} }
select { select {
@apply select select-sm disabled:opacity-40 font-normal placeholder:text-neutral-700 text-white rounded-none; @apply select select-sm disabled:bg-coolgray-200 border-none disabled:opacity-50 font-normal placeholder:text-neutral-700 text-white rounded-none;
} }
.breadcrumbs > ul > li::before { .breadcrumbs > ul > li::before {
@apply text-warning opacity-100; @apply text-warning opacity-100;
@ -47,7 +47,7 @@ button[type="button"] {
@apply hover:bg-coolgray-400 btn h-7 btn-xs border-none bg-coolgray-200 no-animation normal-case text-white rounded; @apply hover:bg-coolgray-400 btn h-7 btn-xs border-none bg-coolgray-200 no-animation normal-case text-white rounded;
} }
button[type="submit"] { button[type="submit"] {
@apply btn btn-xs no-animation h-7 normal-case text-white btn-primary rounded; @apply hover:bg-coolgray-400 btn h-7 btn-xs border-none bg-coolgray-200 no-animation normal-case text-white rounded;
} }
button[isWarning] { button[isWarning] {
@apply bg-error; @apply bg-error;

View File

@ -15,23 +15,32 @@
]) }}"> ]) }}">
<button>Deployments</button> <button>Deployments</button>
</a> </a>
<livewire:project.application.status :application="$application" />
<div class="flex-1"></div> <div class="flex-1"></div>
<div class="dropdown dropdown-bottom"> <div class="dropdown dropdown-bottom dropdown-hover">
<label tabindex="0"> <label tabindex="0" class="flex items-center gap-2 text-sm cursor-pointer hover:text-white"> Links
<x-forms.button>
Open
<x-chevron-down /> <x-chevron-down />
</x-forms.button>
</label> </label>
<ul tabindex="0" <ul tabindex="0"
class="mt-1 text-xs text-white normal-case rounded min-w-max dropdown-content menu bg-coolgray-200"> class="text-xs text-white normal-case rounded min-w-max dropdown-content menu bg-coolgray-200">
<li>
<a target="_blank" class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs"
href="{{ $application->gitBranchLocation }}">
<x-git-icon git="{{ $application->source->getMorphClass() }}" />
Git Repository
</a>
</li>
@if (data_get($application, 'fqdn')) @if (data_get($application, 'fqdn'))
<li> <li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs" target="_blank" <a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs" target="_blank"
href="{{ $application->fqdn }}"> href="{{ $application->fqdn }}">
{{ $application->fqdn }} <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
<x-external-link /> stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>{{ $application->fqdn }}
</a> </a>
</li> </li>
@endif @endif
@ -40,9 +49,16 @@ class="mt-1 text-xs text-white normal-case rounded min-w-max dropdown-content me
@if (config('app.env') === 'local') @if (config('app.env') === 'local')
<li> <li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs" <a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs"
target="_blank" href="http://localhost:{{ explode(':', $port)[0] }}">Port target="_blank" href="http://localhost:{{ explode(':', $port)[0] }}">
{{ explode(':', $port)[0] }} <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
<x-external-link /> stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>{{ $port }}
</a> </a>
</li> </li>
@else @else
@ -50,7 +66,6 @@ class="mt-1 text-xs text-white normal-case rounded min-w-max dropdown-content me
<a class="text-xs hover:no-underline hover:bg-coollabs" target="_blank" <a class="text-xs hover:no-underline hover:bg-coollabs" target="_blank"
href="http://{{ $application->destination->server->ip }}:{{ explode(':', $port)[0] }}">Port href="http://{{ $application->destination->server->ip }}:{{ explode(':', $port)[0] }}">Port
{{ $port }} {{ $port }}
<x-external-link />
</a> </a>
</li> </li>
@endif @endif

View File

@ -5,7 +5,8 @@
]) ])
<span {{ $attributes->merge(['class' => 'flex flex-col']) }}> <span {{ $attributes->merge(['class' => 'flex flex-col']) }}>
<label for={{ $id }}> <label class="label" for={{ $id }}>
<span class="label-text">
@if ($label) @if ($label)
{{ $label }} {{ $label }}
@else @else
@ -14,6 +15,7 @@
@if ($required) @if ($required)
<span class="text-warning">*</span> <span class="text-warning">*</span>
@endif @endif
</span>
</label> </label>
<select {{ $attributes }} wire:model.defer={{ $id }}> <select {{ $attributes }} wire:model.defer={{ $id }}>
{{ $slot }} {{ $slot }}

View File

@ -0,0 +1,18 @@
@props([
'git' => null,
])
@if ($git === 'App\Models\GithubApp')
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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="M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5" />
</svg>
@elseif($git === 'App\Models\GitlabApp')
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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="M21 14l-9 7l-9 -7l3 -11l3 7h6l3 -7z" />
</svg>
@endif

View File

@ -1 +1 @@
<span class="loading loading-bars"></span> <span {{ $attributes->class(['bg-warning loading', 'loading-spinner' => !$attributes->has('class')]) }}></span>

View File

@ -1,13 +1,16 @@
<div> <div>
@if ($this->activity) @if ($this->activity)
@if ($header) @if ($header)
<div class="flex gap-2">
<h2>Logs</h2> <h2>Logs</h2>
@if ($isPollingActive)
<x-loading />
@endif
</div>
@endif @endif
<div <div
class="scrollbar flex flex-col-reverse w-full overflow-y-auto border border-solid rounded border-coolgray-300 max-h-[32rem] p-4 text-xs text-white"> class="scrollbar flex flex-col-reverse w-full overflow-y-auto border border-solid rounded border-coolgray-300 max-h-[32rem] p-4 text-xs text-white">
@if ($isPollingActive)
<span class="loading loading-bars"></span>
@endif
<pre class="font-mono whitespace-pre-wrap" @if ($isPollingActive) wire:poll.2000ms="polling" @endif>{{ \App\Actions\CoolifyTask\RunRemoteProcess::decodeOutput($this->activity) }}</pre> <pre class="font-mono whitespace-pre-wrap" @if ($isPollingActive) wire:poll.2000ms="polling" @endif>{{ \App\Actions\CoolifyTask\RunRemoteProcess::decodeOutput($this->activity) }}</pre>
{{-- @else {{-- @else
<pre class="whitespace-pre-wrap">Output will be here...</pre> --}} <pre class="whitespace-pre-wrap">Output will be here...</pre> --}}

View File

@ -1,7 +1,6 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h2>Notifications</h2> <h2>Notifications</h2>
<x-forms.button isHighlighted class="text-white normal-case btn btn-xs no-animation btn-primary" <x-forms.button class="text-white normal-case btn btn-xs no-animation btn-primary" wire:click="sendTestNotification">
wire:click="sendTestNotification">
Send Test Notifications Send Test Notifications
</x-forms.button> </x-forms.button>
</div> </div>

View File

@ -1,40 +1,114 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@if ($application->status === 'running') <div class="dropdown dropdown-bottom dropdown-hover">
<div class="dropdown dropdown-bottom"> <label tabindex="0" class="flex items-center gap-2 cursor-pointer hover:text-white"> Actions
<x-forms.button isHighlighted tabindex="0" class="">
Actions
<x-chevron-down /> <x-chevron-down />
</x-forms.button>
<ul tabindex="0"
class="mt-1 text-xs text-white normal-case rounded min-w-max dropdown-content menu bg-coolgray-200">
<li>
<div class="rounded-none hover:bg-coollabs" wire:click='deploy'>Restart</div>
</li>
<li>
<div class="rounded-none hover:bg-coollabs" wire:click='deploy(true)'>Force deploy without cache</div>
</li>
<li>
<div class="rounded-none hover:bg-red-500" wire:click='stop'>Stop</div>
</li>
</ul>
</div>
@else
<div class="dropdown dropdown-bottom">
<label tabindex="0">
<x-forms.button isHighlighted>
Actions
<x-chevron-down />
</x-forms.button>
</label> </label>
@if ($application->status === 'running')
<ul tabindex="0" <ul tabindex="0"
class="mt-1 text-xs text-white normal-case rounded min-w-max dropdown-content menu bg-coolgray-200"> class="text-xs text-white normal-case rounded min-w-max dropdown-content menu bg-coolgray-200">
<li> <li>
<div class="rounded-none hover:bg-coollabs" wire:click='deploy'>Deploy</div> <div class="rounded-none hover:bg-coollabs" wire:click='deploy'><svg xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" 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="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
<path d="M12 9l0 3" />
<path d="M12 15l.01 0" />
</svg>Restart</div>
</li> </li>
<li> <li>
<div class="rounded-none hover:bg-coollabs" wire:click='deploy(true)'>Deploy without cache</div> <div class="rounded-none hover:bg-coollabs" wire:click='deploy(true, true)'><svg
xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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="M9 9v-1a3 3 0 0 1 6 0v1" />
<path d="M8 9h8a6 6 0 0 1 1 3v3a5 5 0 0 1 -10 0v-3a6 6 0 0 1 1 -3" />
<path d="M3 13l4 0" />
<path d="M17 13l4 0" />
<path d="M12 20l0 -6" />
<path d="M4 19l3.35 -2" />
<path d="M20 19l-3.35 -2" />
<path d="M4 7l3.75 2.4" />
<path d="M20 7l-3.75 2.4" />
</svg>Force deploy (with
debug)
</div>
</li>
<li>
<div class="rounded-none hover:bg-coollabs" wire:click='deploy(true)'><svg
xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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.983 8.978c3.955 -.182 7.017 -1.446 7.017 -2.978c0 -1.657 -3.582 -3 -8 -3c-1.661 0 -3.204 .19 -4.483 .515m-2.783 1.228c-.471 .382 -.734 .808 -.734 1.257c0 1.22 1.944 2.271 4.734 2.74" />
<path
d="M4 6v6c0 1.657 3.582 3 8 3c.986 0 1.93 -.067 2.802 -.19m3.187 -.82c1.251 -.53 2.011 -1.228 2.011 -1.99v-6" />
<path d="M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4" />
<path d="M3 3l18 18" />
</svg>Force deploy (without
cache)
</div>
</li>
<li>
<div class="rounded-none hover:bg-red-500" wire:click='stop'><svg xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" 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="M8 13v-7.5a1.5 1.5 0 0 1 3 0v6.5" />
<path d="M11 5.5v-2a1.5 1.5 0 1 1 3 0v8.5" />
<path d="M14 5.5a1.5 1.5 0 0 1 3 0v6.5" />
<path
d="M17 7.5a1.5 1.5 0 0 1 3 0v8.5a6 6 0 0 1 -6 6h-2h.208a6 6 0 0 1 -5.012 -2.7a69.74 69.74 0 0 1 -.196 -.3c-.312 -.479 -1.407 -2.388 -3.286 -5.728a1.5 1.5 0 0 1 .536 -2.022a1.867 1.867 0 0 1 2.28 .28l1.47 1.47" />
</svg>Stop</div>
</li> </li>
</ul> </ul>
@else
<ul tabindex="0"
class="text-xs text-white normal-case rounded min-w-max dropdown-content menu bg-coolgray-200">
<li>
<div class="rounded-none hover:bg-coollabs" wire:click='deploy'><svg
xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>Deploy</div>
</li>
<li>
<div class="rounded-none hover:bg-coollabs" wire:click='deploy(true, true)'><svg
xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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="M9 9v-1a3 3 0 0 1 6 0v1" />
<path d="M8 9h8a6 6 0 0 1 1 3v3a5 5 0 0 1 -10 0v-3a6 6 0 0 1 1 -3" />
<path d="M3 13l4 0" />
<path d="M17 13l4 0" />
<path d="M12 20l0 -6" />
<path d="M4 19l3.35 -2" />
<path d="M20 19l-3.35 -2" />
<path d="M4 7l3.75 2.4" />
<path d="M20 7l-3.75 2.4" />
</svg>Force deploy (with
debug)
</div> </div>
</li>
<li>
<div class="rounded-none hover:bg-coollabs" wire:click='deploy(true)'><svg
xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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.983 8.978c3.955 -.182 7.017 -1.446 7.017 -2.978c0 -1.657 -3.582 -3 -8 -3c-1.661 0 -3.204 .19 -4.483 .515m-2.783 1.228c-.471 .382 -.734 .808 -.734 1.257c0 1.22 1.944 2.271 4.734 2.74" />
<path
d="M4 6v6c0 1.657 3.582 3 8 3c.986 0 1.93 -.067 2.802 -.19m3.187 -.82c1.251 -.53 2.011 -1.228 2.011 -1.99v-6" />
<path d="M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4" />
<path d="M3 3l18 18" />
</svg>Force deploy (without
cache)
</div>
</li>
</ul>
@endif @endif
</div> </div>
</div>

View File

@ -2,14 +2,15 @@
<h2>Logs</h2> <h2>Logs</h2>
<livewire:project.application.deployment-navbar :activity="$activity" :application="$application" :deployment_uuid="$deployment_uuid" /> <livewire:project.application.deployment-navbar :activity="$activity" :application="$application" :deployment_uuid="$deployment_uuid" />
@if (data_get($activity, 'properties.status') === 'in_progress') @if (data_get($activity, 'properties.status') === 'in_progress')
<div class="pt-2 text-sm">Deployment is <div class="flex items-center gap-1 pt-2 text-sm">Deployment is
<span class="text-warning">{{ Str::headline(data_get($activity, 'properties.status')) }}</span>. Logs will <div class="text-warning"> {{ Str::headline(data_get($activity, 'properties.status')) }}.</div>
be updated <x-loading class="loading-ring" />
automatically.
</div> </div>
<div>Logs will be updated automatically.</div>
@else @else
<div class="pt-2 text-sm">Deployment is <span <div class="pt-2 text-sm">Deployment is <span
class="text-warning">{{ Str::headline(data_get($activity, 'properties.status')) }}</span>.</div> class="text-warning">{{ Str::headline(data_get($activity, 'properties.status')) }}</span>.
</div>
@endif @endif
<div <div
class="scrollbar flex flex-col-reverse w-full overflow-y-auto border border-solid rounded border-coolgray-300 max-h-[32rem] p-4 mt-4 text-xs text-white"> class="scrollbar flex flex-col-reverse w-full overflow-y-auto border border-solid rounded border-coolgray-300 max-h-[32rem] p-4 mt-4 text-xs text-white">

View File

@ -2,14 +2,11 @@
@if ($skip == 0) wire:poll.5000ms='reload_deployments' @endif> @if ($skip == 0) wire:poll.5000ms='reload_deployments' @endif>
<h2 class="pt-4">Deployments <span class="text-xs">({{ $deployments_count }})</span></h2> <h2 class="pt-4">Deployments <span class="text-xs">({{ $deployments_count }})</span></h2>
@if (count($deployments) === 0) @if (count($deployments) === 0)
<x-forms.button isHighlighted wire:click="load_deployments({{ $default_take }})">Load Deployments <x-forms.button wire:click="load_deployments({{ $default_take }})">Load Deployments
</x-forms.button> </x-forms.button>
@endif @endif
<div wire:loading wire:target='load_deployments'>
<x-loading />
</div>
@if ($show_next) @if ($show_next)
<x-forms.button isHighlighted wire:click="load_deployments({{ $default_take }})">Show More <x-forms.button wire:click="load_deployments({{ $default_take }})">Show More
</x-forms.button> </x-forms.button>
@endif @endif
@foreach ($deployments as $deployment) @foreach ($deployments as $deployment)

View File

@ -1,8 +1,8 @@
<div> <div>
<h2>Destination</h2> <h2>Destination</h2>
<div class="text-sm">The destination server / network where your application will be deployed to.</div> <div class="text-sm">The destination server / network where your application will be deployed to.</div>
<div class="py-4"> <div class="py-4 text-sm">
<p>Server: {{ data_get($destination, 'server.name') }}</p> <p>Server: {{ data_get($destination, 'server.name') }}</p>
<p>Destination: {{ $destination->network }}</p> <p>Destination Network: {{ $destination->network }}</p>
</div> </div>
</div> </div>

View File

@ -1,12 +1,10 @@
<form wire:submit.prevent='submit' class="flex flex-col max-w-fit"> <form wire:submit.prevent='submit' class="flex flex-col max-w-fit">
<div class="flex gap-2"> <div class="flex items-end justify-center gap-2">
<x-forms.input placeholder="NODE_ENV" noDirty id="key" label="Name" required /> <x-forms.input placeholder="NODE_ENV" noDirty id="key" label="Name" required />
<x-forms.input placeholder="production" noDirty id="value" label="Value" required /> <x-forms.input placeholder="production" noDirty id="value" label="Value" required />
<x-forms.checkbox noDirty class="flex-col items-center" id="is_build_time" label="Build Variable?" /> <x-forms.checkbox noDirty class="flex-col text-center w-96" id="is_build_time" label="Build Variable?" />
</div>
<div class="pt-2">
<x-forms.button type="submit"> <x-forms.button type="submit">
Add Add New Variable
</x-forms.button> </x-forms.button>
</div> </div>
</form> </form>

View File

@ -1,17 +1,24 @@
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div> <div>
<h2>Environment Variables</h2> <h2>Environment Variables</h2>
<div class="text-sm">Environment (secrets) configuration. You can set variables for your Preview Deployments as <div class="text-sm">Environment (secrets) variables for normal deployments.</div>
well
here.</div>
</div> </div>
@forelse ($application->environment_variables as $env) @foreach ($application->environment_variables as $env)
<livewire:project.application.environment-variable.show wire:key="environment-{{ $env->id }}" <livewire:project.application.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" /> :env="$env" />
@empty @endforeach
<p>There are no environment variables added for this application.</p> <div class="pt-2 pb-8">
@endforelse
<div class="pt-10">
<livewire:project.application.environment-variable.add /> <livewire:project.application.environment-variable.add />
</div> </div>
<div>
<h3>Preview Deployments</h3>
<div class="text-sm">Environment (secrets) variables for Preview Deployments.</div>
</div>
@foreach ($application->environment_variables_preview as $env)
<livewire:project.application.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" />
@endforeach
<div class="pt-2 pb-8">
<livewire:project.application.environment-variable.add is_preview="true" />
</div>
</div> </div>

View File

@ -1,11 +1,10 @@
<div x-data="{ deleteEnvironment: false }"> <div x-data="{ deleteEnvironment: false }">
<form wire:submit.prevent='submit' class="flex flex-col max-w-fit"> <form wire:submit.prevent='submit' class="flex flex-col max-w-fit">
<div class="flex gap-2"> <div class="flex items-end gap-2">
<x-forms.input label="Name" id="env.key" /> <x-forms.input label="Name" id="env.key" />
<x-forms.input label="Value" id="env.value" /> <x-forms.input label="Value" id="env.value" />
<x-forms.checkbox disabled class="flex-col items-center" id="env.is_build_time" label="Build Variable?" /> <x-forms.checkbox disabled class="flex-col text-center w-96" id="env.is_build_time" label="Build Variable?" />
</div> <div class="flex gap-2">
<div class="pt-2">
<x-forms.button type="submit"> <x-forms.button type="submit">
Update Update
</x-forms.button> </x-forms.button>
@ -13,6 +12,7 @@
Delete Delete
</x-forms.button> </x-forms.button>
</div> </div>
</div>
</form> </form>
<x-naked-modal show="deleteEnvironment" message="Are you sure you want to delete {{ $env->key }}?" /> <x-naked-modal show="deleteEnvironment" message="Are you sure you want to delete {{ $env->key }}?" />
</div> </div>

View File

@ -18,11 +18,11 @@
<div class="pb-6"> <div class="pb-6">
<div class="text-sm">Set Random Domain</div> <div class="text-sm">Set Random Domain</div>
@if ($global_wildcard_domain) @if ($global_wildcard_domain)
<x-forms.button isHighlighted wire:click="generateGlobalRandomDomain">Global Wildcard <x-forms.button wire:click="generateGlobalRandomDomain">Global Wildcard
</x-forms.button> </x-forms.button>
@endif @endif
@if ($project_wildcard_domain) @if ($project_wildcard_domain)
<x-forms.button isHighlighted wire:click="generateProjectRandomDomain">Project Wildcard <x-forms.button wire:click="generateProjectRandomDomain">Project Wildcard
</x-forms.button> </x-forms.button>
@endif @endif
</div> </div>

View File

@ -1,40 +1,69 @@
<div> <div>
<livewire:project.application.preview.form :application="$application" /> <livewire:project.application.preview.form :application="$application" />
<h3>Pull Requests on Git</h3>
<div> <div>
<div class="flex items-center gap-2">
<h3>Pull Requests on Git</h3>
<x-forms.button wire:click="load_prs">Load Pull Requests (open) <x-forms.button wire:click="load_prs">Load Pull Requests (open)
</x-forms.button> </x-forms.button>
</div>
@isset($rate_limit_remaining) @isset($rate_limit_remaining)
<div class="pt-1 text-sm">Requests remaning till rate limited by Git: {{ $rate_limit_remaining }}</div> <div class="pt-1 text-sm">Requests remaning till rate limited by Git: {{ $rate_limit_remaining }}</div>
@endisset @endisset
@if (count($pull_requests) > 0) @if (count($pull_requests) > 0)
<div wire:loading.remove wire:target='load_prs' class="flex gap-4 py-8"> <div wire:loading.remove wire:target='load_prs' class="flex gap-4 py-4">
<div class="overflow-x-auto table-md">
<table class="table">
<thead>
<tr class="text-warning border-coolgray-200">
<th>PR Number</th>
<th>PR Title</th>
<th>Git</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach ($pull_requests as $pull_request) @foreach ($pull_requests as $pull_request)
<div class="flex flex-col gap-4 p-4 text-sm bg-coolgray-200 hover:bg-coolgray-300"> <tr class="border-coolgray-200">
<div class="text-base font-bold text-white">PR #{{ data_get($pull_request, 'number') }} | <th>{{ data_get($pull_request, 'number') }}</th>
{{ data_get($pull_request, 'title') }}</div> <td>{{ data_get($pull_request, 'title') }}</td>
<div class="flex items-center justify-start gap-2"> <td>
<x-forms.button isHighlighted <a target="_blank" class="text-xs"
wire:click="deploy('{{ data_get($pull_request, 'number') }}', '{{ data_get($pull_request, 'html_url') }}')"> href="{{ data_get($pull_request, 'html_url') }}">Open PR on
Deploy
</x-forms.button>
<a target="_blank" class="text-xs" href="{{ data_get($pull_request, 'html_url') }}">Open PR
on
Git Git
<x-external-link /> <x-external-link />
</a> </a>
</div> </td>
</div> <td class="flex items-center justify-center gap-2">
<x-forms.button
wire:click="deploy('{{ data_get($pull_request, 'number') }}', '{{ data_get($pull_request, 'html_url') }}')">
Deploy
</x-forms.button>
</td>
</tr>
@endforeach @endforeach
</tbody>
</table>
</div>
</div> </div>
@endif @endif
</div> </div>
@if ($application->previews->count() > 0) @if ($application->previews->count() > 0)
<h3>Preview Deployments</h3> <h4 class="pt-4">Preview Deployments</h4>
<div class="flex gap-6 text-sm"> <div class="flex gap-6 text-sm">
@foreach ($application->previews as $preview) @foreach ($application->previews as $preview)
<div class="flex flex-col p-4 bg-coolgray-200 " x-init="$wire.loadStatus('{{ data_get($preview, 'pull_request_id') }}')"> <div class="flex flex-col p-4 bg-coolgray-200 " x-init="$wire.loadStatus('{{ data_get($preview, 'pull_request_id') }}')">
<div>PR #{{ data_get($preview, 'pull_request_id') }} | {{ data_get($preview, 'status') }} <div class="flex gap-2">PR #{{ data_get($preview, 'pull_request_id') }} |
@if (data_get($preview, 'status') === 'running')
<div class="flex items-center gap-2">
<div class="badge badge-success badge-xs"></div>
<div class="text-xs font-medium tracking-wide">Running</div>
</div>
@else
<div class="flex items-center gap-2">
<div class="badge badge-error badge-xs"></div>
<div class="text-xs font-medium tracking-wide">Stopped</div>
</div>
@endif
@if (data_get($preview, 'status') !== 'exited') @if (data_get($preview, 'status') !== 'exited')
| <a target="_blank" href="{{ data_get($preview, 'fqdn') }}">Open Preview | <a target="_blank" href="{{ data_get($preview, 'fqdn') }}">Open Preview
<x-external-link /> <x-external-link />
@ -46,7 +75,7 @@
</a> </a>
</div> </div>
<div class="flex items-center gap-2 pt-6"> <div class="flex items-center gap-2 pt-6">
<x-forms.button isHighlighted wire:click="deploy({{ data_get($preview, 'pull_request_id') }})"> <x-forms.button wire:click="deploy({{ data_get($preview, 'pull_request_id') }})">
@if (data_get($preview, 'status') === 'exited') @if (data_get($preview, 'status') === 'exited')
Deploy Deploy
@else @else
@ -58,7 +87,6 @@
Preview Preview
</x-forms.button> </x-forms.button>
@endif @endif
</div> </div>
</div> </div>
@endforeach @endforeach

View File

@ -1,12 +1,9 @@
<div x-init="$wire.loadImages"> <div x-init="$wire.loadImages">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h2>Rollback</h2> <h2>Rollback</h2>
<x-forms.button isHighlighted wire:click='loadImages'>Reload Available Images</x-forms.button> <x-forms.button wire:click='loadImages'>Reload Available Images</x-forms.button>
</div> </div>
<div class="pb-4 text-sm">You can easily rollback to a previously built image quickly.</div> <div class="pb-4 text-sm">You can easily rollback to a previously built image quickly.</div>
<div wire:loading wire:target='loadImages'>
<x-loading />
</div>
<div wire:loading.remove wire:target='loadImages'> <div wire:loading.remove wire:target='loadImages'>
<div class="flex flex-wrap"> <div class="flex flex-wrap">
@forelse ($images as $image) @forelse ($images as $image)
@ -28,8 +25,7 @@
Rollback Rollback
</x-forms.button> </x-forms.button>
@else @else
<x-forms.button isHighlighted <x-forms.button wire:click="rollbackImage('{{ data_get($image, 'tag') }}')">
wire:click="rollbackImage('{{ data_get($image, 'tag') }}')">
Rollback Rollback
</x-forms.button> </x-forms.button>
@endif @endif

View File

@ -1,25 +1,32 @@
<div> <div>
<form wire:submit.prevent='submit' class="flex flex-col"> <form wire:submit.prevent='submit' class="flex flex-col w-96">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h2>Source</h2> <h2>Source</h2>
<x-forms.button type="submit">Save</x-forms.button> <x-forms.button type="submit">Save</x-forms.button>
<a target="_blank" class="hover:no-underline" href="{{ $application->gitBranchLocation }}">
<x-forms.button>
<x-git-icon git="{{ $application->source->getMorphClass() }}" />Open Repository on Git
<x-external-link />
</x-forms.button>
</a>
</div> </div>
<div class="text-sm">Code source of your application.</div> <div class="text-sm">Code source of your application.</div>
<div class="py-4 ">
<a target="_blank" class="hover:no-underline" href="{{ $application->gitCommits }}">
<x-forms.button>Open Commits on Git
<x-external-link />
</x-forms.button>
</a>
<a target="_blank" class="hover:no-underline" href="{{ $application->gitBranchLocation }}">
<x-forms.button>Open Repository on Git
<x-external-link />
</x-forms.button>
</a>
</div>
<x-forms.input placeholder="coollabsio/coolify-example" id="application.git_repository" label="Repository" /> <x-forms.input placeholder="coollabsio/coolify-example" id="application.git_repository" label="Repository" />
<x-forms.input placeholder="main" id="application.git_branch" label="Branch" /> <x-forms.input placeholder="main" id="application.git_branch" label="Branch" />
<div class="flex items-end gap-2 w-96">
<x-forms.input placeholder="HEAD" id="application.git_commit_sha" placeholder="HEAD" label="Commit SHA" /> <x-forms.input placeholder="HEAD" id="application.git_commit_sha" placeholder="HEAD" label="Commit SHA" />
<a target="_blank" class="flex hover:no-underline" href="{{ $application->gitCommits }}">
<x-forms.button><svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" />
<path d="M12 3l0 6" />
<path d="M12 15l0 6" />
</svg>Open Commits on Git
<x-external-link />
</x-forms.button>
</a>
</div>
</form> </form>
</div> </div>

View File

@ -1,13 +1,15 @@
<div wire:poll.5000ms='applicationStatusChanged'> <div wire:poll.5000ms='applicationStatusChanged'>
@if ($application->status === 'running') @if ($application->status === 'running')
<span class="text-xs text-pink-600" wire:loading.delay.longer>Loading current status...</span> <x-loading wire:loading.delay.longer />
<div class="flex items-center gap-2 text-sm" wire:loading.remove.delay.longer> <div class="flex items-center gap-2 text-sm" wire:loading.remove.delay.longer>
<div class="text-xs font-medium tracking-wide text-white badge border-success">Running</div> <div class="badge badge-success badge-xs"></div>
<div class="text-xs font-medium tracking-wide">Running</div>
</div> </div>
@else @else
<span class="text-xs text-pink-600" wire:loading.delay.longer>Loading current status...</span> <x-loading wire:loading.delay.longer />
<div class="flex items-center gap-2 text-sm" wire:loading.remove.delay.longer> <div class="flex items-center gap-2 text-sm" wire:loading.remove.delay.longer>
<div class="text-xs font-medium tracking-wide text-white badge border-error">Stopped</div> <div class="badge badge-error badge-xs"></div>
<div class="text-xs font-medium tracking-wide">Stopped</div>
</div> </div>
@endif @endif
</div> </div>

View File

@ -1,10 +1,10 @@
<form wire:submit.prevent='submit' class="flex flex-col px-2 pt-10 max-w-fit"> <form wire:submit.prevent='submit' class="flex flex-col w-full px-2">
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<x-forms.input placeholder="pv-name" noDirty id="name" label="Name" required /> <x-forms.input placeholder="pv-name" noDirty id="name" label="Name" required />
<x-forms.input placeholder="/root" noDirty id="host_path" label="Source Path" /> <x-forms.input placeholder="/root" noDirty id="host_path" label="Source Path" />
<x-forms.input placeholder="/tmp/root" noDirty id="mount_path" label="Destination Path" required /> <x-forms.input placeholder="/tmp/root" noDirty id="mount_path" label="Destination Path" required />
<x-forms.button type="submit"> <x-forms.button type="submit">
Add Add New Volume
</x-forms.button> </x-forms.button>
</div> </div>
</form> </form>

View File

@ -1,6 +1,11 @@
<div>
<div> <div>
<h2>Storages</h2> <h2>Storages</h2>
<div class="text-sm">Persistent storage to preserve data between deployments.</div> <div class="text-sm">Persistent storage to preserve data between deployments.</div>
<div class="text-sm">Preview Deployments has a <span class='text-helper'>-pr-#PRNumber</span> in their
volume
name, example: <span class='text-helper'>-pr-1</span>.</div>
</div>
<div class="flex flex-col gap-2 py-4"> <div class="flex flex-col gap-2 py-4">
@forelse ($application->persistentStorages as $storage) @forelse ($application->persistentStorages as $storage)
<livewire:project.application.storages.show wire:key="storage-{{ $storage->id }}" :storage="$storage" /> <livewire:project.application.storages.show wire:key="storage-{{ $storage->id }}" :storage="$storage" />

View File

@ -1,10 +1,28 @@
<div> <div>
<h1>Enter a public repository URL</h1> <h1>Enter a public repository URL</h1>
<form class="flex flex-col gap-2" wire:submit.prevent='submit'> <form class="flex flex-col gap-2" wire:submit.prevent='submit'>
<x-forms.checkbox instantSave id="is_static" label="Is it a static site?" /> <div class="flex flex-col gap-2">
<div class="flex gap-2"> <div class="flex flex-col">
<x-forms.input id="repository_url" label="Repository URL" <div class="flex items-end gap-2">
<x-forms.input wire:keypress.enter='load_branches' id="repository_url" label="Repository URL"
helper="<span class='text-helper'>Example</span>https://github.com/coollabsio/coolify-examples => main branch will be selected<br>https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify => nodejs-fastify branch will be selected" /> helper="<span class='text-helper'>Example</span>https://github.com/coollabsio/coolify-examples => main branch will be selected<br>https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify => nodejs-fastify branch will be selected" />
<x-forms.button wire:click.prevent="load_branches">
Check repository
</x-forms.button>
</div>
@if (count($branches) > 0)
<x-forms.select id="selected_branch" label="Branch">
<option value="default" disabled selected>Select a branch</option>
@foreach ($branches as $branch)
<option value="{{ $branch }}">{{ $branch }}</option>
@endforeach
</x-forms.select>
@else
<x-forms.select id="branch" label="Branch" disabled>
<option value="default" selected>Set a repository first</option>
</x-forms.select>
@endif
</div>
@if ($is_static) @if ($is_static)
<x-forms.input id="publish_directory" label="Publish Directory" <x-forms.input id="publish_directory" label="Publish Directory"
helper="If there is a build process involved (like Svelte, React, Next, etc..), please specify the output directory for the build assets." /> helper="If there is a build process involved (like Svelte, React, Next, etc..), please specify the output directory for the build assets." />
@ -12,9 +30,18 @@
<x-forms.input type="number" id="port" label="Port" :readonly="$is_static" <x-forms.input type="number" id="port" label="Port" :readonly="$is_static"
helper="The port your application listens on." /> helper="The port your application listens on." />
@endif @endif
</div> </div>
<x-forms.checkbox instantSave id="is_static" label="Is it a static site?" />
@if (count($branches) > 0)
<x-forms.button type="submit"> <x-forms.button type="submit">
Submit Save
</x-forms.button> </x-forms.button>
@else
<x-forms.button disabled type="submit">
Save
</x-forms.button>
@endif
</form> </form>
</div> </div>

View File

@ -38,7 +38,7 @@
</div> </div>
</div> </div>
@if (!$server->settings->is_validated) @if (!$server->settings->is_validated)
<x-forms.button class="mt-4" isHighlighted wire:click.prevent='validateServer'> <x-forms.button class="mt-4" wire:click.prevent='validateServer'>
Validate Server Validate Server
</x-forms.button> </x-forms.button>
@endif @endif

View File

@ -2,7 +2,7 @@
@if ($server->settings->is_validated) @if ($server->settings->is_validated)
@if ($server->extra_attributes->proxy_status === 'running') @if ($server->extra_attributes->proxy_status === 'running')
<div class="dropdown dropdown-bottom"> <div class="dropdown dropdown-bottom">
<x-forms.button isHighlighted tabindex="0"> <x-forms.button tabindex="0">
Actions Actions
<x-chevron-down /> <x-chevron-down />
</x-forms.button> </x-forms.button>
@ -33,9 +33,9 @@ class="mt-1 text-xs text-white normal-case rounded min-w-max dropdown-content me
</ul> </ul>
</div> </div>
@else @else
<x-forms.button isHighlighted wire:click='deploy'> <svg xmlns="http://www.w3.org/2000/svg" class="icon" <x-forms.button wire:click='deploy'> <svg xmlns="http://www.w3.org/2000/svg" class="icon" width="44"
width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none"
fill="none" stroke-linecap="round" stroke-linejoin="round"> stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" /> <path d="M7 4v16l13 -8z" />
</svg>Start</x-forms.button> </svg>Start</x-forms.button>

View File

@ -2,12 +2,12 @@
@if ($server->settings->is_validated) @if ($server->settings->is_validated)
<div wire:poll.5000ms="proxyStatus"> <div wire:poll.5000ms="proxyStatus">
@if ($server->extra_attributes->proxy_status === 'running') @if ($server->extra_attributes->proxy_status === 'running')
<span class="text-xs text-pink-600" wire:loading.delay.longer>Loading current status...</span> <x-loading wire:loading.delay.longer />
<div class="flex items-center gap-2 text-sm" wire:loading.remove.delay.longer> <div class="flex items-center gap-2 text-sm" wire:loading.remove.delay.longer>
<div class="text-xs font-medium tracking-wide text-white badge border-success">Running</div> <div class="text-xs font-medium tracking-wide text-white badge border-success">Running</div>
</div> </div>
@else @else
<span class="text-xs text-pink-600" wire:loading.delay.longer>Loading current status...</span> <x-loading wire:loading.delay.longer />
<div class="flex items-center gap-2 text-sm" wire:loading.remove.delay.longer> <div class="flex items-center gap-2 text-sm" wire:loading.remove.delay.longer>
<div class="text-xs font-medium tracking-wide text-white badge border-error">Stopped</div> <div class="text-xs font-medium tracking-wide text-white badge border-error">Stopped</div>
</div> </div>

View File

@ -33,7 +33,7 @@
<div class="pb-4"> <div class="pb-4">
<div class="text-sm">You need to register a GitHub App before using this source!</div> <div class="text-sm">You need to register a GitHub App before using this source!</div>
<form> <form>
<x-forms.button isHighlighted x-on:click.prevent="createGithubApp">Register a GitHub <x-forms.button x-on:click.prevent="createGithubApp">Register a GitHub
Application Application
</x-forms.button> </x-forms.button>
</form> </form>

View File

@ -1,5 +1,5 @@
<div> <div class="w-64 -mt-9">
<x-forms.select wire:model="selectedTeamId" class="w-64 select-xs"> <x-forms.select wire:model="selectedTeamId" class="pr-0 select-xs ">
<option value="default" disabled selected>Switch team</option> <option value="default" disabled selected>Switch team</option>
@foreach (auth()->user()->teams as $team) @foreach (auth()->user()->teams as $team)
<option value="{{ $team->id }}">{{ $team->name }}</option> <option value="{{ $team->id }}">{{ $team->name }}</option>

View File

@ -9,6 +9,9 @@
href="{{ route('project.resources', ['environment_name' => request()->route('environment_name'), 'project_uuid' => request()->route('project_uuid')]) }}">{{ request()->route('environment_name') }}</a> href="{{ route('project.resources', ['environment_name' => request()->route('environment_name'), 'project_uuid' => request()->route('project_uuid')]) }}">{{ request()->route('environment_name') }}</a>
</li> </li>
<li>{{ data_get($application, 'name') }}</li> <li>{{ data_get($application, 'name') }}</li>
<li>
<livewire:project.application.status :application="$application" />
</li>
</ul> </ul>
</div> </div>
<x-applications.navbar :application="$application" /> <x-applications.navbar :application="$application" />

View File

@ -9,6 +9,9 @@
href="{{ route('project.resources', ['environment_name' => request()->route('environment_name'), 'project_uuid' => request()->route('project_uuid')]) }}">{{ request()->route('environment_name') }}</a> href="{{ route('project.resources', ['environment_name' => request()->route('environment_name'), 'project_uuid' => request()->route('project_uuid')]) }}">{{ request()->route('environment_name') }}</a>
</li> </li>
<li>{{ data_get($application, 'name') }}</li> <li>{{ data_get($application, 'name') }}</li>
<li>
<livewire:project.application.status :application="$application" />
</li>
</ul> </ul>
</div> </div>
<x-applications.navbar :application="$application" /> <x-applications.navbar :application="$application" />

View File

@ -1,7 +1,7 @@
<x-layout> <x-layout>
<h1>Deployments</h1> <h1>Deployments</h1>
<div class="pb-10 text-sm breadcrumbs"> <div class="pb-10 text-sm breadcrumbs">
<ul>` <ul>
<li><a <li><a
href="{{ route('project.show', ['project_uuid' => request()->route('project_uuid')]) }}">{{ $application->environment->project->name }}</a> href="{{ route('project.show', ['project_uuid' => request()->route('project_uuid')]) }}">{{ $application->environment->project->name }}</a>
</li> </li>
@ -9,6 +9,9 @@
href="{{ route('project.resources', ['environment_name' => request()->route('environment_name'), 'project_uuid' => request()->route('project_uuid')]) }}">{{ request()->route('environment_name') }}</a> href="{{ route('project.resources', ['environment_name' => request()->route('environment_name'), 'project_uuid' => request()->route('project_uuid')]) }}">{{ request()->route('environment_name') }}</a>
</li> </li>
<li>{{ data_get($application, 'name') }}</li> <li>{{ data_get($application, 'name') }}</li>
<li>
<livewire:project.application.status :application="$application" />
</li>
</ul> </ul>
</div> </div>
<x-applications.navbar :application="$application" /> <x-applications.navbar :application="$application" />

View File

@ -22,7 +22,7 @@
<h2>Invite a new member</h2> <h2>Invite a new member</h2>
<form class="flex items-center gap-2"> <form class="flex items-center gap-2">
<x-forms.input type="email" name="email" placeholder="Email" /> <x-forms.input type="email" name="email" placeholder="Email" />
<x-forms.button isHighlighted>Invite</x-forms.button> <x-forms.button>Invite</x-forms.button>
</form> </form>
</div> </div>
</x-layout> </x-layout>