Compare commits

...

21 Commits

Author SHA1 Message Date
Andras Bacsai
93fb14884e Merge pull request #1711 from coollabsio/next
Refactor server validation and installation logic
2024-02-05 15:13:56 +01:00
Andras Bacsai
26ccc4afb4 Refactor server validation and installation logic 2024-02-05 15:13:39 +01:00
Andras Bacsai
5fda1bb932 Merge pull request #1710 from coollabsio/next
v4.0.0-beta.207
2024-02-05 14:57:21 +01:00
Andras Bacsai
409ba8a1bb Refactor application deployment logic 2024-02-05 14:47:06 +01:00
Andras Bacsai
49f5240ff8 fix: better server validation and installation process
fix: add destination to queue deployment
feat: force start deployment
2024-02-05 14:40:54 +01:00
Andras Bacsai
0c3ed3d393 Update BunnyCDN sync and version numbers 2024-02-05 10:17:40 +01:00
Andras Bacsai
6e3dc474f2 Merge pull request #1702 from coollabsio/next
v4.0.0-beta.206
2024-02-05 10:06:59 +01:00
Andras Bacsai
d3eb87561e Fix styling issue in tag links 2024-02-05 10:00:53 +01:00
Andras Bacsai
8b58c8f856 Add tags to show and index views 2024-02-05 09:51:44 +01:00
Andras Bacsai
8c60ef5bd6 Update link in error message to the correct documentation 2024-02-04 17:00:13 +01:00
Andras Bacsai
1d59383c78 feat: clone to env 2024-02-04 16:54:12 +01:00
Andras Bacsai
60f590454d Update application deployment status in job handling 2024-02-04 14:40:23 +01:00
Andras Bacsai
dcb61a553e Merge pull request #1706 from piscis/patch-1
fix: Wrap tags and avoid horizontal overflow
2024-02-04 14:39:55 +01:00
Andras Bacsai
e06e31642f Refactor modal component and add new functionality 2024-02-04 14:07:08 +01:00
Andras Bacsai
9dfce48380 Add private_keys array initialization and define additional private properties 2024-02-04 13:50:24 +01:00
Andras Bacsai
8eed87e2f7 Update main class with mx-auto 2024-02-04 13:50:16 +01:00
Alex
d56d4eb8fc fix: Wrap tags and avoid horizontal overflow 2024-02-04 13:15:39 +01:00
Andras Bacsai
fd32cd04ab Refactor invoice payment failure handling in webhooks.php 2024-02-04 12:23:00 +01:00
Andras Bacsai
1d3b7ffd3b Refactor tags functionality and improve user experience 2024-02-03 12:44:18 +01:00
Andras Bacsai
0b5baf60a5 fix: tags 2024-02-03 12:39:07 +01:00
Andras Bacsai
bc31df6fb2 Update version numbers to 4.0.0-beta.206 2024-02-02 14:52:24 +01:00
58 changed files with 711 additions and 312 deletions

View File

@@ -43,6 +43,7 @@ class InstallDocker
"echo 'Restarting Docker Engine...'",
"ls -l /tmp"
]);
return remote_process($command, $server);
} else {
if ($supported_os_type->contains('debian')) {
$command = $command->merge([
@@ -89,7 +90,6 @@ class InstallDocker
"echo 'Done!'",
]);
}
return remote_process($command, $server);
}
}

View File

@@ -48,7 +48,7 @@ class SyncBunny extends Command
$versions = "versions.json";
PendingRequest::macro('storage', function ($fileName) use($that) {
PendingRequest::macro('storage', function ($fileName) use ($that) {
$headers = [
'AccessKey' => env('BUNNY_STORAGE_API_KEY'),
'Accept' => 'application/json',
@@ -76,23 +76,26 @@ class SyncBunny extends Command
}
if ($only_template) {
$this->info('About to sync service-templates.json to BunnyCDN.');
}
if ($only_version) {
$this->info('About to sync versions.json to BunnyCDN.');
}
$confirmed = confirm('Are you sure you want to sync?');
if (!$confirmed) {
return;
}
if ($only_template) {
$confirmed = confirm("Are you sure you want to sync?");
if (!$confirmed) {
return;
}
Http::pool(fn (Pool $pool) => [
$pool->storage(fileName: "$parent_dir/templates/$service_template")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$service_template"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$service_template"),
]);
$this->info('Service template uploaded & purged...');
return;
}
if ($only_version) {
} else if ($only_version) {
$this->info('About to sync versions.json to BunnyCDN.');
$file = file_get_contents("$parent_dir/$versions");
$json = json_decode($file, true);
$actual_version = data_get($json, 'coolify.v4.version');
$confirmed = confirm("Are you sure you want to sync to {$actual_version}?");
if (!$confirmed) {
return;
}
Http::pool(fn (Pool $pool) => [
$pool->storage(fileName: "$parent_dir/$versions")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"),
@@ -101,6 +104,7 @@ class SyncBunny extends Command
return;
}
Http::pool(fn (Pool $pool) => [
$pool->storage(fileName: "$parent_dir/$compose_file")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"),
$pool->storage(fileName: "$parent_dir/$compose_file_prod")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"),

View File

@@ -73,12 +73,17 @@ class Deploy extends Controller
$message->push("Tag {$tag} not found.");
continue;
}
$resources = $found_tag->resources()->get();
if ($resources->count() === 0) {
$applications = $found_tag->applications();
$services = $found_tag->services();
if ($applications->count() === 0 && $services->count() === 0) {
$message->push("No resources found for tag {$tag}.");
continue;
}
foreach ($resources as $resource) {
foreach ($applications as $resource) {
$return_message = $this->deploy_resource($resource, $force);
$message = $message->merge($return_message);
}
foreach ($services as $resource) {
$return_message = $this->deploy_resource($resource, $force);
$message = $message->merge($return_message);
}

View File

@@ -122,7 +122,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($source) {
$this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first();
}
$this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first();
$this->server = Server::find($this->application_deployment_queue->server_id);
$this->destination = $this->server->destinations()->where('id', $this->application_deployment_queue->destination_id)->first();
$this->server = $this->mainServer = $this->destination->server;
$this->serverUser = $this->server->user;
$this->basedir = $this->application->generateBaseDir($this->deployment_uuid);
@@ -166,10 +167,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
public function handle(): void
{
$this->application_deployment_queue->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
// Generate custom host<->ip mapping
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
if (!is_null($allContainers)) {
@@ -565,12 +562,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile();
$this->build_image();
// if ($this->application->additional_destinations) {
// $this->push_to_docker_registry();
// $this->deploy_to_additional_destinations();
// } else {
$this->rolling_update();
// }
}
private function deploy_nixpacks_buildpack()
{
@@ -795,7 +787,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
private function deploy_to_additional_destinations()
{
if (str($this->application->additional_destinations)->isEmpty()) {
return;
}
$destination_ids = collect(str($this->application->additional_destinations)->explode(','));
if ($this->server->isSwarm()) {
$this->application_deployment_queue->addLogEntry("Additional destinations are not supported in swarm mode.");
return;
}
if ($destination_ids->contains($this->destination->id)) {
ray('Same destination found in additional destinations. Skipping.');
return;
}
foreach ($destination_ids as $destination_id) {
$destination = StandaloneDocker::find($destination_id);
$server = $destination->server;
@@ -803,11 +806,21 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->application_deployment_queue->addLogEntry("Skipping deployment to {$server->name}. Not in the same team?!");
continue;
}
$this->server = $server;
$this->application_deployment_queue->addLogEntry("Deploying to {$this->server->name}.");
$this->prepare_builder_image();
$this->generate_image_names();
$this->rolling_update();
// ray('Deploying to additional destination: ', $server->name);
$deployment_uuid = new Cuid2();
queue_application_deployment(
deployment_uuid: $deployment_uuid,
application: $this->application,
server: $server,
destination: $destination,
no_questions_asked: true,
);
$this->application_deployment_queue->addLogEntry("Deploying to additional server: {$server->name}. Click here to see the deployment status: " . route('project.application.deployment.show', [
'project_uuid' => data_get($this->application, 'environment.project.uuid'),
'application_uuid' => data_get($this->application, 'uuid'),
'deployment_uuid' => $deployment_uuid,
'environment_name' => data_get($this->application, 'environment.name'),
]));
}
}
private function set_base_dir()
@@ -1511,11 +1524,13 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
'status' => $status,
]);
}
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
}
if ($status === ApplicationDeploymentStatus::FAILED->value) {
$this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
return;
}
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
// $this->deploy_to_additional_destinations();
$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
}
}

View File

@@ -15,7 +15,7 @@ class ActivityMonitor extends Component
public $isPollingActive = false;
protected $activity;
protected $listeners = ['newMonitorActivity'];
protected $listeners = ['activityMonitor' => 'newMonitorActivity'];
public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished')
{

View File

@@ -12,6 +12,7 @@ use Livewire\Component;
class Index extends Component
{
protected $listeners = ['serverInstalled' => 'validateServer'];
public string $currentState = 'welcome';
public ?string $selectedServerType = null;
@@ -93,7 +94,11 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->serverPublicKey = $this->createdServer->privateKey->publicKey();
return $this->validateServer('localhost');
} elseif ($this->selectedServerType === 'remote') {
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
if (isDev()) {
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->get();
} else {
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
}
if ($this->privateKeys->count() > 0) {
$this->selectedExistingPrivateKey = $this->privateKeys->first()->id;
}
@@ -190,6 +195,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->createdServer->addInitialNetwork();
$this->validateServer();
}
public function installServer()
{
$this->dispatch('validateServer', true);
}
public function validateServer()
{
try {
@@ -228,7 +237,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->dockerInstallationStarted = true;
$activity = InstallDocker::run($this->createdServer);
$this->dispatch('installDocker');
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
$this->dockerInstallationStarted = false;
return handleError(error: $e, livewire: $this);

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Livewire;
use App\Models\User;
use Livewire\Component;
use Spatie\Activitylog\Models\Activity;
class NewActivityMonitor extends Component
{
public ?string $header = null;
public $activityId;
public $eventToDispatch = 'activityFinished';
public $isPollingActive = false;
protected $activity;
protected $listeners = ['newActivityMonitor' => 'newMonitorActivity'];
public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished')
{
$this->activityId = $activityId;
$this->eventToDispatch = $eventToDispatch;
$this->hydrateActivity();
$this->isPollingActive = true;
}
public function hydrateActivity()
{
$this->activity = Activity::find($this->activityId);
}
public function polling()
{
$this->hydrateActivity();
// $this->setStatus(ProcessStatus::IN_PROGRESS);
$exit_code = data_get($this->activity, 'properties.exitCode');
if ($exit_code !== null) {
// if ($exit_code === 0) {
// // $this->setStatus(ProcessStatus::FINISHED);
// } else {
// // $this->setStatus(ProcessStatus::ERROR);
// }
$this->isPollingActive = false;
if ($this->eventToDispatch !== null) {
if (str($this->eventToDispatch)->startsWith('App\\Events\\')) {
$causer_id = data_get($this->activity, 'causer_id');
$user = User::find($causer_id);
if ($user) {
foreach ($user->teams as $team) {
$teamId = $team->id;
$this->eventToDispatch::dispatch($teamId);
}
}
return;
}
$this->dispatch($this->eventToDispatch);
ray('Dispatched event: ' . $this->eventToDispatch);
}
}
}
}

View File

@@ -7,8 +7,6 @@ use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;
use Livewire\Component;
class DeploymentNavbar extends Component
@@ -37,7 +35,15 @@ class DeploymentNavbar extends Component
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->dispatch('refreshQueue');
}
public function force_start()
{
try {
force_start_deployment($this->application_deployment_queue);
} catch (\Throwable $e) {
ray($e);
return handleError($e, $this);
}
}
public function cancel()
{
try {
@@ -67,7 +73,6 @@ class DeploymentNavbar extends Component
'current_process_id' => null,
'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
]);
// queue_next_deployment($this->application);
}
}
}

View File

@@ -46,26 +46,6 @@ class Heading extends Component
$this->deploy(force_rebuild: true);
}
public function deployNew()
{
if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {
$this->dispatch('error', 'Please load a Compose file first.');
return;
}
$this->setDeploymentUuid();
queue_application_deployment(
application: $this->application,
deployment_uuid: $this->deploymentUuid,
force_rebuild: false,
is_new_deployment: true,
);
return redirect()->route('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_name' => $this->parameters['environment_name'],
]);
}
public function deploy(bool $force_rebuild = false)
{
if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {

View File

@@ -22,12 +22,12 @@ class CloneMe extends Component
public ?int $selectedDestination = null;
public ?Server $server = null;
public $resources = [];
public string $newProjectName = '';
public string $newName = '';
protected $messages = [
'selectedServer' => 'Please select a server.',
'selectedDestination' => 'Please select a server & destination.',
'newProjectName' => 'Please enter a name for the new project.',
'newName' => 'Please enter a name for the new project or environment.',
];
public function mount($project_uuid)
{
@@ -36,7 +36,7 @@ class CloneMe extends Component
$this->environment = $this->project->environments->where('name', $this->environment_name)->first();
$this->project_id = $this->project->id;
$this->servers = currentTeam()->servers;
$this->newProjectName = str($this->project->name . '-clone-' . (string)new Cuid2(7))->slug();
$this->newName = str($this->project->name . '-clone-' . (string)new Cuid2(7))->slug();
}
public function render()
@@ -46,34 +46,50 @@ class CloneMe extends Component
public function selectServer($server_id, $destination_id)
{
if ($server_id == $this->selectedServer && $destination_id == $this->selectedDestination) {
$this->selectedServer = null;
$this->selectedDestination = null;
$this->server = null;
return;
}
$this->selectedServer = $server_id;
$this->selectedDestination = $destination_id;
$this->server = $this->servers->where('id', $server_id)->first();
}
public function clone()
public function clone(string $type)
{
try {
$this->validate([
'selectedDestination' => 'required',
'newProjectName' => 'required',
'newName' => 'required',
]);
$foundProject = Project::where('name', $this->newProjectName)->first();
if ($foundProject) {
throw new \Exception('Project with the same name already exists.');
}
$newProject = Project::create([
'name' => $this->newProjectName,
'team_id' => currentTeam()->id,
'description' => $this->project->description . ' (clone)',
]);
if ($this->environment->name !== 'production') {
$newProject->environments()->create([
'name' => $this->environment->name,
if ($type === 'project') {
$foundProject = Project::where('name', $this->newName)->first();
if ($foundProject) {
throw new \Exception('Project with the same name already exists.');
}
$project = Project::create([
'name' => $this->newName,
'team_id' => currentTeam()->id,
'description' => $this->project->description . ' (clone)',
]);
if ($this->environment->name !== 'production') {
$project->environments()->create([
'name' => $this->environment->name,
]);
}
$environment = $project->environments->where('name', $this->environment->name)->first();
} else {
$foundEnv = $this->project->environments()->where('name', $this->newName)->first();
if ($foundEnv) {
throw new \Exception('Environment with the same name already exists.');
}
$project = $this->project;
$environment = $this->project->environments()->create([
'name' => $this->newName,
]);
}
$newEnvironment = $newProject->environments->where('name', $this->environment->name)->first();
// Clone Applications
$applications = $this->environment->applications;
$databases = $this->environment->databases();
$services = $this->environment->services;
@@ -83,7 +99,7 @@ class CloneMe extends Component
'uuid' => $uuid,
'fqdn' => generateFqdn($this->server, $uuid),
'status' => 'exited',
'environment_id' => $newEnvironment->id,
'environment_id' => $environment->id,
// This is not correct, but we need to set it to something
'destination_id' => $this->selectedDestination,
]);
@@ -110,7 +126,7 @@ class CloneMe extends Component
'uuid' => $uuid,
'status' => 'exited',
'started_at' => null,
'environment_id' => $newEnvironment->id,
'environment_id' => $environment->id,
'destination_id' => $this->selectedDestination,
]);
$newDatabase->save();
@@ -136,7 +152,7 @@ class CloneMe extends Component
$uuid = (string)new Cuid2(7);
$newService = $service->replicate()->fill([
'uuid' => $uuid,
'environment_id' => $newEnvironment->id,
'environment_id' => $environment->id,
'destination_id' => $this->selectedDestination,
]);
$newService->save();
@@ -153,8 +169,8 @@ class CloneMe extends Component
$newService->parse();
}
return redirect()->route('project.resource.index', [
'project_uuid' => $newProject->uuid,
'environment_name' => $newEnvironment->name,
'project_uuid' => $project->uuid,
'environment_name' => $environment->name,
]);
} catch (\Exception $e) {
return handleError($e, $this);

View File

@@ -58,19 +58,19 @@ class Heading extends Component
{
if ($this->database->type() === 'standalone-postgresql') {
$activity = StartPostgresql::run($this->database);
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('activityMonitor', $activity->id);
} else if ($this->database->type() === 'standalone-redis') {
$activity = StartRedis::run($this->database);
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('activityMonitor', $activity->id);
} else if ($this->database->type() === 'standalone-mongodb') {
$activity = StartMongodb::run($this->database);
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('activityMonitor', $activity->id);
} else if ($this->database->type() === 'standalone-mysql') {
$activity = StartMysql::run($this->database);
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('activityMonitor', $activity->id);
} else if ($this->database->type() === 'standalone-mariadb') {
$activity = StartMariadb::run($this->database);
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('activityMonitor', $activity->id);
}
}
}

View File

@@ -129,7 +129,7 @@ class Import extends Component
if (!empty($this->importCommands)) {
$activity = remote_process($this->importCommands, $this->server, ignore_errors: true);
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('activityMonitor', $activity->id);
}
} catch (\Throwable $e) {
$this->validated = false;

View File

@@ -9,6 +9,7 @@ use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Illuminate\Support\Collection;
use Livewire\Component;
use Spatie\Url\Url;
use Illuminate\Support\Str;
@@ -18,7 +19,7 @@ class GithubPrivateRepositoryDeployKey extends Component
public $current_step = 'private_keys';
public $parameters;
public $query;
public $private_keys;
public $private_keys =[];
public int $private_key_id;
public int $port = 3000;
@@ -33,6 +34,11 @@ class GithubPrivateRepositoryDeployKey extends Component
public $build_pack = 'nixpacks';
public bool $show_is_static = true;
private object $repository_url_parsed;
private GithubApp|GitlabApp|string $git_source = 'other';
private ?string $git_host = null;
private string $git_repository;
protected $rules = [
'repository_url' => 'required',
'branch' => 'required|string',
@@ -49,10 +55,7 @@ class GithubPrivateRepositoryDeployKey extends Component
'publish_directory' => 'Publish directory',
'build_pack' => 'Build pack',
];
private object $repository_url_parsed;
private GithubApp|GitlabApp|string $git_source = 'other';
private ?string $git_host = null;
private string $git_repository;
public function mount()
{

View File

@@ -57,7 +57,7 @@ class Navbar extends Component
}
$this->service->parse();
$activity = StartService::run($this->service);
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('activityMonitor', $activity->id);
}
public function stop(bool $forceCleanup = false)
{
@@ -82,6 +82,6 @@ class Navbar extends Component
StopService::run($this->service);
$this->service->parse();
$activity = StartService::run($this->service);
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('activityMonitor', $activity->id);
}
}

View File

@@ -5,7 +5,7 @@ namespace App\Livewire\Project\Service;
use App\Models\ServiceApplication;
use Livewire\Component;
class Application extends Component
class ServiceApplicationView extends Component
{
public ServiceApplication $application;
public $parameters;
@@ -20,7 +20,7 @@ class Application extends Component
];
public function render()
{
return view('livewire.project.service.application');
return view('livewire.project.service.service-application-view');
}
public function instantSave()
{

View File

@@ -115,7 +115,7 @@ class ExecuteContainerCommand extends Component
$exec = "docker exec {$this->container} {$cmd}";
}
$activity = remote_process([$exec], $this->server, ignore_errors: true);
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -37,12 +37,13 @@ class Tags extends Component
return handleError($e, $this);
}
}
public function deleteTag($id, $name)
public function deleteTag(string $id)
{
try {
$found_more_tags = Tag::where(['name' => $name, 'team_id' => currentTeam()->id])->first();
$this->resource->tags()->detach($id);
if ($found_more_tags->resources()->get()->count() == 0) {
$found_more_tags = Tag::where(['id' => $id, 'team_id' => currentTeam()->id])->first();
if ($found_more_tags->applications()->count() == 0 && $found_more_tags->services()->count() == 0){
$found_more_tags->delete();
}
$this->refresh();
@@ -53,6 +54,7 @@ class Tags extends Component
public function refresh()
{
$this->resource->load(['tags']);
$this->tags = Tag::ownedByCurrentTeam()->get();
$this->new_tag = null;
}
public function submit()

View File

@@ -31,7 +31,7 @@ class RunCommand extends Component
$this->validate();
try {
$activity = remote_process([$this->command], Server::where('uuid', $this->server)->first(), ignore_errors: true);
$this->dispatch('newMonitorActivity', $activity->id);
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -2,7 +2,6 @@
namespace App\Livewire\Server;
use App\Actions\Server\InstallDocker;
use App\Models\Server;
use Livewire\Component;
@@ -14,7 +13,8 @@ class Form extends Component
public ?string $wildcard_domain = null;
public int $cleanup_after_percentage;
public bool $dockerInstallationStarted = false;
protected $listeners = ['serverRefresh'];
protected $listeners = ['serverInstalled'];
protected $rules = [
'server.name' => 'required',
@@ -49,9 +49,10 @@ class Form extends Component
$this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
}
public function serverRefresh($install = true)
public function serverInstalled()
{
$this->validateServer($install);
$this->server->refresh();
$this->server->settings->refresh();
}
public function instantSave()
{
@@ -64,13 +65,6 @@ class Form extends Component
return handleError($e, $this);
}
}
public function installDocker()
{
$this->dispatch('installDocker');
$this->dockerInstallationStarted = true;
$activity = InstallDocker::run($this->server);
$this->dispatch('newMonitorActivity', $activity->id);
}
public function checkLocalhostConnection()
{
$uptime = $this->server->validateConnection();
@@ -80,48 +74,13 @@ class Form extends Component
$this->server->settings->is_usable = true;
$this->server->settings->save();
} else {
$this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.');
$this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.');
return;
}
}
public function validateServer($install = true)
{
try {
$uptime = $this->server->validateConnection();
if (!$uptime) {
$install && $this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.');
return;
}
$supported_os_type = $this->server->validateOS();
if (!$supported_os_type) {
$install && $this->dispatch('error', 'Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.');
return;
}
$dockerInstalled = $this->server->validateDockerEngine();
if ($dockerInstalled) {
$install && $this->dispatch('success', 'Docker Engine is installed.<br> Checking version.');
} else {
$install && $this->installDocker();
return;
}
$dockerVersion = $this->server->validateDockerEngineVersion();
if ($dockerVersion) {
$install && $this->dispatch('success', 'Docker Engine version is 22+.');
} else {
$install && $this->installDocker();
return;
}
if ($this->server->isSwarm()) {
$swarmInstalled = $this->server->validateDockerSwarm();
if ($swarmInstalled) {
$install && $this->dispatch('success', 'Docker Swarm is initiated.');
}
}
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->dispatch('proxyStatusUpdated');
}
$this->dispatch('validateServer', $install);
}
public function submit()

View File

@@ -71,7 +71,7 @@ class Deploy extends Component
{
try {
$activity = StartProxy::run($this->server);
$this->dispatch('newMonitorActivity', $activity->id, ProxyStatusChanged::class);
$this->dispatch('activityMonitor', $activity->id, ProxyStatusChanged::class);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -12,10 +12,8 @@ class Status extends Component
public Server $server;
public bool $polling = false;
public int $numberOfPolls = 0;
protected $listeners = ['proxyStatusUpdated', 'startProxyPolling'];
public function mount() {
}
public function startProxyPolling()
{
$this->checkProxy();

View File

@@ -11,7 +11,7 @@ class Show extends Component
use AuthorizesRequests;
public ?Server $server = null;
public $parameters = [];
protected $listeners = ['proxyStatusUpdated' => '$refresh'];
protected $listeners = ['serverInstalled' => '$refresh'];
public function mount()
{
$this->parameters = get_route_parameters();

View File

@@ -39,7 +39,7 @@ class ShowPrivateKey extends Component
if ($uptime) {
$this->dispatch('success', 'Server is reachable.');
} else {
$this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/configuration#openssh-server">documentation</a> for further help.');
$this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh#openssh">documentation</a> for further help.');
return;
}
} catch (\Throwable $e) {

View File

@@ -0,0 +1,105 @@
<?php
namespace App\Livewire\Server;
use App\Models\Server;
use Livewire\Component;
class ValidateAndInstall extends Component
{
public Server $server;
public int $number_of_tries = 0;
public int $max_tries = 1;
public bool $install = true;
public $uptime = null;
public $supported_os_type = null;
public $docker_installed = null;
public $docker_version = null;
public $error = null;
protected $listeners = ['validateServer' => 'init', 'validateDockerEngine', 'validateServerNow' => 'validateServer'];
public function init(bool $install = true)
{
$this->install = $install;
$this->uptime = null;
$this->supported_os_type = null;
$this->docker_installed = null;
$this->docker_version = null;
$this->error = null;
$this->number_of_tries = 0;
$this->dispatch('validateServerNow');
}
public function validateServer()
{
try {
$this->validateConnection();
$this->validateOS();
$this->validateDockerEngine();
if ($this->server->isSwarm()) {
$swarmInstalled = $this->server->validateDockerSwarm();
if ($swarmInstalled) {
$this->dispatch('success', 'Docker Swarm is initiated.');
}
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function validateConnection()
{
$this->uptime = $this->server->validateConnection();
if (!$this->uptime) {
$this->error = 'Server is not reachable. Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.';
return;
}
}
public function validateOS()
{
$this->supported_os_type = $this->server->validateOS();
if (!$this->supported_os_type) {
$this->error = 'Server OS type is not supported. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
return;
}
}
public function validateDockerEngine()
{
$this->docker_installed = $this->server->validateDockerEngine();
if (!$this->docker_installed) {
if ($this->install) {
ray($this->number_of_tries, $this->max_tries);
if ($this->number_of_tries == $this->max_tries) {
$this->error = 'Docker Engine could not be installed. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
return;
} else {
$activity = $this->server->installDocker();
$this->number_of_tries++;
$this->dispatch('newActivityMonitor', $activity->id, 'validateDockerEngine');
return;
}
} else {
$this->error = 'Docker Engine is not installed. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
return;
}
} else {
$this->validateDockerVersion();
}
}
public function validateDockerVersion()
{
$this->docker_version = $this->server->validateDockerEngineVersion();
if ($this->docker_version) {
$this->dispatch('serverInstalled');
$this->dispatch('success', 'Server validated successfully.');
} else {
$this->error = 'Docker Engine version is not 22+. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
return;
}
}
public function render()
{
return view('livewire.server.validate-and-install');
}
}

View File

@@ -9,15 +9,30 @@ use Livewire\Component;
class Show extends Component
{
public $tags;
public Tag $tag;
public $resources;
public $applications;
public $services;
public $webhook = null;
public $deployments_per_tag_per_server = [];
public function mount()
{
$this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name');
$tag = $this->tags->where('name', request()->tag_name)->first();
if (!$tag) {
return redirect()->route('tags.index');
}
$this->webhook = generatTagDeployWebhook($tag->name);
$this->applications = $tag->applications()->get();
$this->services = $tag->services()->get();
$this->tag = $tag;
$this->get_deployments();
}
public function get_deployments()
{
try {
$resource_ids = $this->resources->pluck('id');
$resource_ids = $this->applications->pluck('id');
$this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->whereIn('application_id', $resource_ids)->get([
"id",
"application_id",
@@ -35,7 +50,11 @@ class Show extends Component
public function redeploy_all()
{
try {
$this->resources->each(function ($resource) {
$this->applications->each(function ($resource) {
$deploy = new Deploy();
$deploy->deploy_resource($resource);
});
$this->services->each(function ($resource) {
$deploy = new Deploy();
$deploy->deploy_resource($resource);
});
@@ -44,17 +63,7 @@ class Show extends Component
return handleError($e, $this);
}
}
public function mount()
{
$tag = Tag::ownedByCurrentTeam()->where('name', request()->tag_name)->first();
if (!$tag) {
return redirect()->route('tags.index');
}
$this->webhook = generatTagDeployWebhook($tag->name);
$this->resources = $tag->resources()->get();
$this->tag = $tag;
$this->get_deployments();
}
public function render()
{
return view('livewire.tags.show');

View File

@@ -216,6 +216,9 @@ class Application extends BaseModel
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function project() {
return data_get($this, 'environment.project');
}
public function team()
{
return data_get($this, 'environment.project.team');

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use App\Actions\Server\InstallDocker;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Notifications\Server\Revived;
@@ -411,6 +412,11 @@ class Server extends BaseModel
return true;
}
public function installDocker()
{
$activity = InstallDocker::run($this);
return $activity;
}
public function validateDockerEngine($throwError = false)
{
$dockerBinary = instant_remote_process(["command -v docker"], $this, false);

View File

@@ -16,6 +16,10 @@ class Service extends BaseModel
{
return 'service';
}
public function project()
{
return data_get($this, 'environment.project');
}
public function team()
{
return data_get($this, 'environment.project.team');

View File

@@ -24,9 +24,8 @@ class Tag extends BaseModel
{
return $this->morphedByMany(Application::class, 'taggable');
}
public function resources() {
return $this->applications();
public function services()
{
return $this->morphedByMany(Service::class, 'taggable');
}
}

View File

@@ -1,23 +1,35 @@
<?php
use App\Enums\ApplicationDeploymentStatus;
use App\Jobs\ApplicationDeploymentJob;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use App\Models\StandaloneDocker;
use Spatie\Url\Url;
function queue_application_deployment(Application $application, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null)
function queue_application_deployment(Application $application, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, Server $server = null, StandaloneDocker $destination = null)
{
$application_id = $application->id;
$deployment_link = Url::fromString($application->link() . "/deployment/{$deployment_uuid}");
$deployment_url = $deployment_link->getPath();
$server_id = $application->destination->server->id;
$server_name = $application->destination->server->name;
$destination_id = $application->destination->id;
if ($server) {
$server_id = $server->id;
$server_name = $server->name;
}
if ($destination) {
$destination_id = $destination->id;
}
$deployment = ApplicationDeploymentQueue::create([
'application_id' => $application_id,
'application_name' => $application->name,
'server_id' => $server_id,
'server_name' => $server_name,
'destination_id' => $destination_id,
'deployment_uuid' => $deployment_uuid,
'deployment_url' => $deployment_url,
'pull_request_id' => $pull_request_id,
@@ -28,18 +40,39 @@ function queue_application_deployment(Application $application, string $deployme
'git_type' => $git_type
]);
if (next_queuable($server_id, $application_id)) {
if ($no_questions_asked) {
$deployment->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id,
));
} else if (next_queuable($server_id, $application_id)) {
$deployment->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id,
));
}
}
function force_start_deployment(ApplicationDeploymentQueue $deployment)
{
$deployment->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id,
));
}
function queue_next_deployment(Application $application)
{
$server_id = $application->destination->server_id;
$next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', 'queued')->get()->sortBy('created_at')->first();
if ($next_found) {
$next_found->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $next_found->id,
));

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.205',
'release' => '4.0.0-beta.207',
// 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.205';
return '4.0.0-beta.207';

View File

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

View File

@@ -76,7 +76,7 @@ a {
}
.box {
@apply flex p-2 transition-colors cursor-pointer min-h-[4rem] bg-coolgray-100 hover:bg-coollabs-100 hover:text-white hover:no-underline min-w-[24rem];
@apply flex p-2 transition-colors cursor-pointer min-h-[4rem] bg-coolgray-100 hover:bg-coollabs-100 hover:text-white hover:no-underline;
}
.box-without-bg {

View File

@@ -4,15 +4,22 @@
'isErrorButton' => false,
'disabled' => false,
'action' => 'delete',
'content' => null,
])
<div x-data="{ modalOpen: false }" @keydown.escape.window="modalOpen = false" :class="{ 'z-40': modalOpen }"
class="relative w-auto h-auto">
@if ($disabled)
<x-forms.button isError disabled>{{ $buttonTitle }}</x-forms.button>
@elseif ($isErrorButton)
<x-forms.button isError @click="modalOpen=true">{{ $buttonTitle }}</x-forms.button>
@if ($content)
<div @click="modalOpen=true">
{{ $content }}
</div>
@else
<x-forms.button @click="modalOpen=true">{{ $buttonTitle }}</x-forms.button>
@if ($disabled)
<x-forms.button isError disabled>{{ $buttonTitle }}</x-forms.button>
@elseif ($isErrorButton)
<x-forms.button isError @click="modalOpen=true">{{ $buttonTitle }}</x-forms.button>
@else
<x-forms.button @click="modalOpen=true">{{ $buttonTitle }}</x-forms.button>
@endif
@endif
<template x-teleport="body">
<div x-show="modalOpen" class="fixed top-0 left-0 z-[99] flex items-center justify-center w-screen h-screen"
@@ -30,13 +37,13 @@
class="relative w-full py-6 border rounded shadow-lg bg-coolgray-100 px-7 border-coolgray-300 sm:max-w-lg">
<div class="flex items-center justify-between pb-3">
<h3 class="text-2xl font-bold">{{ $title }}</h3>
<button @click="modalOpen=false"
{{-- <button @click="modalOpen=false"
class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 text-white rounded-full hover:bg-coolgray-300">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</button> --}}
</div>
<div class="relative w-auto pb-8">
{{ $slot }}
@@ -48,14 +55,13 @@
<div class="flex-1"></div>
@if ($isErrorButton)
<x-forms.button @click="modalOpen=false" class="w-24" isError type="button"
wire:click.prevent='{{ $action }}'>Continue
wire:click.prevent="{{ $action }}">Continue
</x-forms.button>
@else
<x-forms.button @click="modalOpen=false" class="w-24" isHighlighted type="button"
wire:click.prevent='{{ $action }}'>Continue
wire:click.prevent="{{ $action }}">Continue
</x-forms.button>
@endif
</div>
</div>
</div>

View File

@@ -258,7 +258,8 @@
<div class="flex items-start gap-4 text-xl tracking-tight">Need official support for
your self-hosted instance?
<x-forms.button>
<a class="font-bold text-white hover:no-underline" href="{{ config('coolify.contact') }}">Contact
<a class="font-bold text-white hover:no-underline"
href="{{ config('coolify.contact') }}">Contact
Us</a>
</x-forms.button>
</div>

View File

@@ -1,31 +1,37 @@
@props(['closeWithX' => 'false', 'fullScreen' => 'false'])
<div x-data="{
slideOverOpen: false
}" class="relative w-auto h-auto">
{{ $slot }}
<template x-teleport="body">
<div x-show="slideOverOpen" @keydown.window.escape="slideOverOpen=false" class="relative z-[99]">
<div x-show="slideOverOpen" @click="slideOverOpen = false" class="fixed inset-0 bg-black bg-opacity-60"></div>
<div x-show="slideOverOpen" @if (!$closeWithX) @keydown.window.escape="slideOverOpen=false" @endif
class="relative z-[99]">
<div x-show="slideOverOpen" @if (!$closeWithX) @click="slideOverOpen = false" @endif
class="fixed inset-0 bg-black bg-opacity-60"></div>
<div class="fixed inset-0 overflow-hidden">
<div class="absolute inset-0 overflow-hidden">
<div class="fixed inset-y-0 right-0 flex max-w-full pl-10">
<div x-show="slideOverOpen" @click.away="slideOverOpen = false"
<div x-show="slideOverOpen"
@if (!$closeWithX) @click.away="slideOverOpen = false" @endif
x-transition:enter="transform transition ease-in-out duration-100 sm:duration-300"
x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0"
x-transition:leave="transform transition ease-in-out duration-100 sm:duration-300"
x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full"
class="w-screen max-w-md">
@class([
'max-w-md w-screen' => !$fullScreen,
'max-w-7xl w-screen' => $fullScreen,
])>
<div
class="flex flex-col h-full py-6 overflow-hidden border-l shadow-lg bg-base-100 border-neutral-800">
<div class="px-4 pb-10 sm:px-5">
<div class="px-4 pb-4 sm:px-5">
<div class="flex items-start justify-between pb-1">
<h2 class="text-2xl leading-6" id="slide-over-title">
<h2 class="text-3xl leading-6" id="slide-over-title">
{{ $title }}</h2>
<div class="flex items-center h-auto ml-3">
<button class="icon" @click="slideOverOpen=false"
class="absolute top-0 right-0 z-30 flex items-center justify-center px-3 py-2 mt-4 mr-2 space-x-1 text-xs font-normal border-none rounded">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" fill="none"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
>
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M6 18L18 6M6 6l12 12"></path>
</svg>

View File

@@ -170,6 +170,7 @@
}
})
window.Livewire.on('installDocker', () => {
console.log('Installing Docker...');
installDocker.showModal();
})
});

View File

@@ -1,15 +1,5 @@
@extends('layouts.base')
@section('body')
<x-modal noSubmit modalId="installDocker">
<x-slot:modalBody>
<livewire:activity-monitor header="Docker Installation Logs" />
</x-slot:modalBody>
<x-slot:modalSubmit>
<x-forms.button onclick="installDocker.close()" type="submit">
Close
</x-forms.button>
</x-slot:modalSubmit>
</x-modal>
@if (isSubscriptionActive() || isDev())
<div title="Send us feedback or get help!" class="fixed top-0 right-0 p-2 px-4 pt-4 mt-auto text-xs">
<button class="flex items-center justify-center gap-2" wire:click="help" onclick="help.showModal()">

View File

@@ -12,7 +12,7 @@
<x-navbar-subscription />
@endif
<main class="main max-w-screen-2xl">
<main class="mx-auto main max-w-screen-2xl">
{{ $slot }}
</main>
@endsection

View File

@@ -5,7 +5,8 @@
<h1 class="text-5xl font-bold">Welcome to Coolify</h1>
<p class="py-6 text-xl text-center">Let me help you to set the basics.</p>
<div class="flex justify-center ">
<x-forms.button class="justify-center box" wire:click="$set('currentState','explanation')">Get Started
<x-forms.button class="justify-center w-64 box" wire:click="$set('currentState','explanation')">Get
Started
</x-forms.button>
</div>
@endif
@@ -31,7 +32,7 @@
Telegram, Email, etc.) when something goes wrong, or an action needed from your side.</p>
</x-slot:explanation>
<x-slot:actions>
<x-forms.button class="justify-center box" wire:click="explanation">Next
<x-forms.button class="justify-center w-64 box" wire:click="explanation">Next
</x-forms.button>
</x-slot:actions>
</x-boarding-step>
@@ -43,11 +44,11 @@
or on a <x-highlighted text="Remote Server" />?
</x-slot:question>
<x-slot:actions>
<x-forms.button class="justify-center box" wire:target="setServerType('localhost')"
<x-forms.button class="justify-center w-64 box" wire:target="setServerType('localhost')"
wire:click="setServerType('localhost')">Localhost
</x-forms.button>
<x-forms.button class="justify-center box" wire:target="setServerType('remote')"
<x-forms.button class="justify-center w-64 box " wire:target="setServerType('remote')"
wire:click="setServerType('remote')">Remote Server
</x-forms.button>
@if (!$serverReachable)
@@ -57,9 +58,10 @@
'root' or skip the boarding process and add a new private key manually to Coolify and to the
server.
<br />
Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.
Check this <a target="_blank" class="underline"
href="https://coolify.io/docs/server/openssh">documentation</a> for further help.
<x-forms.input readonly id="serverPublicKey"></x-forms.input>
<x-forms.button class="box" wire:target="setServerType('localhost')"
<x-forms.button class="w-64 box" wire:target="setServerType('localhost')"
wire:click="setServerType('localhost')">Check again
</x-forms.button>
@endif
@@ -83,10 +85,10 @@
Do you have your own SSH Private Key?
</x-slot:question>
<x-slot:actions>
<x-forms.button class="justify-center box" wire:target="setPrivateKey('own')"
<x-forms.button class="justify-center w-64 box" wire:target="setPrivateKey('own')"
wire:click="setPrivateKey('own')">Yes
</x-forms.button>
<x-forms.button class="justify-center box" wire:target="setPrivateKey('create')"
<x-forms.button class="justify-center w-64 box" wire:target="setPrivateKey('create')"
wire:click="setPrivateKey('create')">No (create one for me)
</x-forms.button>
@if (count($privateKeys) > 0)
@@ -119,7 +121,7 @@
There are already servers available for your Team. Do you want to use one of them?
</x-slot:question>
<x-slot:actions>
<x-forms.button class="justify-center box" wire:click="createNewServer">No (create one for me)
<x-forms.button class="justify-center w-64 box" wire:click="createNewServer">No (create one for me)
</x-forms.button>
<div>
<form wire:submit='selectExistingServer' class="flex flex-col w-full gap-4 lg:w-96">
@@ -139,7 +141,7 @@
'root' or skip the boarding process and add a new private key manually to Coolify and to the
server.
<x-forms.input readonly id="serverPublicKey"></x-forms.input>
<x-forms.button class="box" wire:target="validateServer" wire:click="validateServer">Check
<x-forms.button class="w-64 box" wire:target="validateServer" wire:click="validateServer">Check
again
</x-forms.button>
@endif
@@ -231,12 +233,16 @@
Could not find Docker Engine on your server. Do you want me to install it for you?
</x-slot:question>
<x-slot:actions>
<x-forms.button class="justify-center box" wire:click="installDocker">
Let's do it!</x-forms.button>
@if ($dockerInstallationStarted)
<x-forms.button class="justify-center box" wire:click="dockerInstalledOrSkipped">
Validate Server & Continue</x-forms.button>
@endif
<x-slide-over closeWithX fullScreen>
<x-slot:title>Configuring Server</x-slot:title>
<x-slot:content>
<livewire:server.validate-and-install :server="$this->createdServer" />
</x-slot:content>
<x-forms.button @click="slideOverOpen=true" class="font-bold box w-96"
wire:click.prevent='installServer' isHighlighted>
Let's do it!
</x-forms.button>
</x-slide-over>
</x-slot:actions>
<x-slot:explanation>
<p>This will install the latest Docker Engine on your server, configure a few things to be able
@@ -246,7 +252,6 @@
documentation</a>.</p>
</x-slot:explanation>
</x-boarding-step>
@endif
</div>
<div>
@@ -289,7 +294,7 @@
@endif
</x-slot:question>
<x-slot:actions>
<x-forms.button class="justify-center box" wire:click="createNewProject">Let's create a new
<x-forms.button class="justify-center w-64 box" wire:click="createNewProject">Let's create a new
one!</x-forms.button>
<div>
@if (count($projects) > 0)
@@ -322,7 +327,7 @@
I will redirect you to the new resource page, where you can create your first resource.
</x-slot:question>
<x-slot:actions>
<div class="items-center justify-center box" wire:click="showNewResource">Let's do
<div class="items-center justify-center w-64 box" wire:click="showNewResource">Let's do
it!</div>
</x-slot:actions>
<x-slot:explanation>

View File

@@ -0,0 +1,18 @@
@php use App\Actions\CoolifyTask\RunRemoteProcess; @endphp
<div>
@if ($this->activity)
@if (isset($header))
<div class="flex gap-2 pb-2">
{{ $header }}
@if ($isPollingActive)
<x-loading />
@endif
</div>
@endif
<div
class="scrollbar flex flex-col-reverse w-full overflow-y-auto border border-solid rounded border-coolgray-300 max-h-[32rem] p-4 pt-6 text-xs text-white">
<pre class="font-mono whitespace-pre-wrap" @if ($isPollingActive) wire:poll.1000ms="polling" @endif>{{ RunRemoteProcess::decodeOutput($this->activity) }}</pre>
</div>
@endif
</div>

View File

@@ -5,8 +5,12 @@
@else
<x-forms.button wire:click.prevent="show_debug">Show Debug Logs</x-forms.button>
@endif
@if (data_get($application_deployment_queue, 'status') === 'queued')
<x-forms.button wire:click.prevent="force_start">Force Start</x-forms.button>
@endif
@if (data_get($application_deployment_queue, 'status') === 'in_progress' ||
data_get($application_deployment_queue, 'status') === 'queued')
<x-forms.button isError wire:click.prevent="cancel">Cancel Deployment</x-forms.button>
<x-forms.button isError wire:click.prevent="cancel">Cancel</x-forms.button>
@endif
</div>

View File

@@ -106,18 +106,6 @@
</svg>
Deploy
</button>
{{-- @if (isDev())
<button wire:click='deployNew'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy (new)
</button>
@endif --}}
@endif
@endif
</div>

View File

@@ -1,19 +1,17 @@
<form wire:submit='clone'>
<form>
<div class="flex flex-col">
<h1>Clone</h1>
<div class="subtitle ">Quickly clone all resources to a new project</div>
</div>
<div class="flex items-end gap-2">
<x-forms.input required id="newProjectName" label="New Project Name" />
<x-forms.button isHighlighted type="submit">Clone</x-forms.button>
<div class="subtitle ">Quickly clone all resources to a new project or environment</div>
</div>
<x-forms.input required id="newName" label="New Name" />
<x-forms.button isHighlighted wire:click="clone('project')" class="mt-4">Clone to a new Project</x-forms.button>
<x-forms.button isHighlighted wire:click="clone('environment')" class="mt-4">Clone to a new Environment</x-forms.button>
<h3 class="pt-4 pb-2">Servers</h3>
<div>Choose the server and network to clone the resources to.</div>
<div class="flex flex-col gap-4">
@foreach ($servers->sortBy('id') as $server)
<div class="p-4">
<h4>{{ $server->name }}</h4>
<h5>{{ $server->description }}</h5>
<div class="pt-4 pb-2">Docker Networks</div>
<div class="grid grid-cols-1 gap-2 pb-4 lg:grid-cols-4">
@foreach ($server->destinations() as $destination)
@@ -30,9 +28,9 @@
<h3 class="pt-4 pb-2">Resources</h3>
<div>These will be cloned to the new project</div>
<div class="grid grid-cols-1 gap-2 p-4 ">
<div class="grid grid-cols-1 gap-2 pt-4 opacity-95 lg:grid-cols-2 xl:grid-cols-3">
@foreach ($environment->applications->sortBy('name') as $application)
<div>
<div class="cursor-default box-without-bg bg-coolgray-100 group">
<div class="flex flex-col">
<div class="font-bold text-white">{{ $application->name }}</div>
<div class="description">{{ $application->description }}</div>
@@ -40,7 +38,7 @@
</div>
@endforeach
@foreach ($environment->databases()->sortBy('name') as $database)
<div>
<div class="cursor-default box-without-bg bg-coolgray-100 group">
<div class="flex flex-col">
<div class="font-bold text-white">{{ $database->name }}</div>
<div class="description">{{ $database->description }}</div>
@@ -48,7 +46,7 @@
</div>
@endforeach
@foreach ($environment->services->sortBy('name') as $service)
<div>
<div class="cursor-default box-without-bg bg-coolgray-100 group">
<div class="flex flex-col">
<div class="font-bold text-white">{{ $service->name }}</div>
<div class="description">{{ $service->description }}</div>

View File

@@ -8,7 +8,7 @@
<li class="step">Select a Repository, Branch & Save</li>
</ul>
<div class="flex flex-col justify-center gap-2 text-left xl:flex-row">
@foreach ($private_keys as $key)
@forelse ($private_keys as $key)
@if ($private_key_id == $key->id)
<div class="gap-2 py-4 cursor-pointer group hover:bg-coollabs bg-coolgray-200"
wire:click.defer="setPrivateKey('{{ $key->id }}')" wire:key="{{ $key->id }}">
@@ -32,7 +32,16 @@
</div>
</div>
@endif
@endforeach
@empty
<div class="flex flex-col items-center justify-center gap-2">
<div class="text-neutral-500">
No private keys found.
</div>
<a href="{{ route('security.private-key.index') }}">
<x-forms.button>Create a new private key</x-forms.button>
</a>
</div>
@endforelse
</div>
@endif
@if ($current_step === 'repository')

View File

@@ -46,7 +46,7 @@
@else
<div x-data="searchComponent()">
<x-forms.input placeholder="Search for name, fqdn..." class="w-full" x-model="search" />
<div class="grid gap-4 pt-4 lg:grid-cols-4">
<div class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3">
<template x-for="item in filteredApplications" :key="item.id">
<span class="relative">
<a class="h-24 box group" :href="item.hrefLink">

View File

@@ -28,7 +28,7 @@
<div class="w-full pl-8">
@isset($serviceApplication)
<div x-cloak x-show="activeTab === 'general'" class="h-full">
<livewire:project.service.application :application="$serviceApplication" />
<livewire:project.service.service-application-view :application="$serviceApplication" />
</div>
<div x-cloak x-show="activeTab === 'storages'">
<div class="flex items-center gap-2">

View File

@@ -11,10 +11,15 @@
<div>
<div class="grid grid-cols-1 gap-2 pb-4 lg:grid-cols-4">
@foreach ($server->destinations() as $destination)
<div class="flex flex-col gap-2 box" wire:click="cloneTo('{{ data_get($destination, 'id') }}')">
<div class="font-bold text-white">{{ $server->name }}</div>
<div>{{ $destination->name }}</div>
</div>
<x-new-modal action="cloneTo({{ data_get($destination, 'id') }})">
<x:slot name="content">
<div class="flex flex-col gap-2 box">
<div class="font-bold text-white">{{ $server->name }}</div>
<div>{{ $destination->name }}</div>
</div>
</x:slot>
<div>You are about to clone this resource.</div>
</x-new-modal>
@endforeach
</div>
</div>
@@ -32,10 +37,15 @@
@forelse ($projects as $project)
<div class="flex flex-row flex-wrap gap-2">
@foreach ($project->environments as $environment)
<div class="flex flex-col gap-2 box" wire:click="moveTo('{{ data_get($environment, 'id') }}')">
<div class="font-bold text-white">{{ $project->name }}</div>
<div><span class="text-warning">{{ $environment->name }}</span> environment</div>
</div>
<x-new-modal action="moveTo({{ data_get($environment, 'id') }})">
<x:slot name="content">
<div class="flex flex-col gap-2 box">
<div class="font-bold text-white">{{ $project->name }}</div>
<div><span class="text-warning">{{ $environment->name }}</span> environment</div>
</div>
</x:slot>
<div>You are about to move this resource.</div>
</x-new-modal>
@endforeach
</div>
@empty

View File

@@ -4,7 +4,7 @@
@forelse ($this->resource->tags as $tagId => $tag)
<div class="px-2 py-1 text-center text-white select-none w-fit bg-coolgray-100 hover:bg-coolgray-200">
{{ $tag->name }}
<svg wire:click="deleteTag('{{ $tag->id }}','{{ $tag->name }}')"
<svg wire:click="deleteTag('{{ $tag->id }}')"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
class="inline-block w-3 h-3 rounded cursor-pointer stroke-current hover:bg-red-500">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
@@ -17,16 +17,19 @@
<form wire:submit='submit' class="flex items-end gap-2 pt-4">
<div class="w-64">
<x-forms.input label="Create new or assign existing tags"
helper="You add more at once with space seperated list: web api something<br><br>If the tag does not exists, it will be created." wire:model="new_tag" />
helper="You add more at once with space seperated list: web api something<br><br>If the tag does not exists, it will be created."
wire:model="new_tag" />
</div>
<x-forms.button type="submit">Add</x-forms.button>
</form>
<h3 class="pt-4">Already defined tags</h3>
<div>Click to quickly add</div>
<div class="flex gap-2 pt-4">
@foreach ($tags as $tag)
<x-forms.button wire:click="addTag('{{ $tag->id }}','{{ $tag->name }}')">
{{ $tag->name }}</x-forms.button>
@endforeach
</div>
@if ($tags->count() > 0)
<h3 class="pt-4">Already defined tags</h3>
<div>Click to quickly add one.</div>
<div class="flex gap-2 pt-4">
@foreach ($tags as $tag)
<x-forms.button wire:click="addTag('{{ $tag->id }}','{{ $tag->name }}')">
{{ $tag->name }}</x-forms.button>
@endforeach
</div>
@endif
</div>

View File

@@ -1,5 +1,5 @@
<div>
<form wire:submit='submit' class="flex flex-col">
<form wire:submit.prevent='submit' class="flex flex-col">
<div class="flex gap-2">
<h2>General</h2>
@if ($server->id === 0)
@@ -18,10 +18,17 @@
Server is reachable and validated.
@endif
@if ((!$server->settings->is_reachable || !$server->settings->is_usable) && $server->id !== 0)
<x-forms.button class="mt-8 mb-4 font-bold box-without-bg bg-coollabs hover:bg-coollabs-100"
wire:click.prevent='validateServer' isHighlighted>
Validate Server & Install Docker Engine
</x-forms.button>
<x-slide-over closeWithX fullScreen>
<x-slot:title>Configuring Server</x-slot:title>
<x-slot:content>
<livewire:server.validate-and-install :server="$server" />
</x-slot:content>
<x-forms.button @click="slideOverOpen=true"
class="w-full mt-8 mb-4 font-bold box-without-bg bg-coollabs hover:bg-coollabs-100"
wire:click.prevent='validateServer' isHighlighted>
Validate Server & Install Docker Engine
</x-forms.button>
</x-slide-over>
@endif
@if ((!$server->settings->is_reachable || !$server->settings->is_usable) && $server->id === 0)
<x-forms.button class="mt-8 mb-4 font-bold box-without-bg bg-coollabs hover:bg-coollabs-100"

View File

@@ -1,14 +1,4 @@
<div>
<x-modal modalId="installDocker">
<x-slot:modalBody>
<livewire:activity-monitor header="Docker Installation Logs" />
</x-slot:modalBody>
<x-slot:modalSubmit>
<x-forms.button onclick="installDocker.close()" type="submit">
Close
</x-forms.button>
</x-slot:modalSubmit>
</x-modal>
<x-server.navbar :server="$server" :parameters="$parameters" />
<livewire:server.form :server="$server" />
<livewire:server.delete :server="$server" />

View File

@@ -0,0 +1,88 @@
<div class="flex flex-col gap-2">
@if ($uptime)
<div class="flex w-64 gap-2">Server is reachable: <svg class="w-5 h-5 text-success" viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg">
<g fill="currentColor">
<path
d="m237.66 85.26l-128.4 128.4a8 8 0 0 1-11.32 0l-71.6-72a8 8 0 0 1 0-11.31l24-24a8 8 0 0 1 11.32 0l36.68 35.32a8 8 0 0 0 11.32 0l92.68-91.32a8 8 0 0 1 11.32 0l24 23.6a8 8 0 0 1 0 11.31"
opacity=".2" />
<path
d="m243.28 68.24l-24-23.56a16 16 0 0 0-22.58 0L104 136l-.11-.11l-36.64-35.27a16 16 0 0 0-22.57.06l-24 24a16 16 0 0 0 0 22.61l71.62 72a16 16 0 0 0 22.63 0l128.4-128.38a16 16 0 0 0-.05-22.67M103.62 208L32 136l24-24l.11.11l36.64 35.27a16 16 0 0 0 22.52 0L208.06 56L232 79.6Z" />
</g>
</svg></div>
@else
@if ($error)
<div class="flex w-64 gap-2">Server is reachable: <svg class="w-5 h-5 text-error" viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M208.49 191.51a12 12 0 0 1-17 17L128 145l-63.51 63.49a12 12 0 0 1-17-17L111 128L47.51 64.49a12 12 0 0 1 17-17L128 111l63.51-63.52a12 12 0 0 1 17 17L145 128Z" />
</svg></div>
@else
<div class="w-64"><x-loading text="Server is reachable: " /></div>
@endif
@endif
@if ($uptime)
@if ($supported_os_type)
<div class="flex w-64 gap-2">Supported OS type: <svg class="w-5 h-5 text-success" viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg">
<g fill="currentColor">
<path
d="m237.66 85.26l-128.4 128.4a8 8 0 0 1-11.32 0l-71.6-72a8 8 0 0 1 0-11.31l24-24a8 8 0 0 1 11.32 0l36.68 35.32a8 8 0 0 0 11.32 0l92.68-91.32a8 8 0 0 1 11.32 0l24 23.6a8 8 0 0 1 0 11.31"
opacity=".2" />
<path
d="m243.28 68.24l-24-23.56a16 16 0 0 0-22.58 0L104 136l-.11-.11l-36.64-35.27a16 16 0 0 0-22.57.06l-24 24a16 16 0 0 0 0 22.61l71.62 72a16 16 0 0 0 22.63 0l128.4-128.38a16 16 0 0 0-.05-22.67M103.62 208L32 136l24-24l.11.11l36.64 35.27a16 16 0 0 0 22.52 0L208.06 56L232 79.6Z" />
</g>
</svg></div>
@else
@if ($error)
<div class="flex w-64 gap-2">Server is reachable: <svg class="w-5 h-5 text-error" viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M208.49 191.51a12 12 0 0 1-17 17L128 145l-63.51 63.49a12 12 0 0 1-17-17L111 128L47.51 64.49a12 12 0 0 1 17-17L128 111l63.51-63.52a12 12 0 0 1 17 17L145 128Z" />
</svg></div>
@else
<div class="w-64"><x-loading text="Server is reachable:" /></div>
@endif
@endif
@endif
@if ($uptime && $supported_os_type)
@if ($docker_installed)
<div class="flex w-64 gap-2">Docker is installed: <svg class="w-5 h-5 text-success" viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg">
<g fill="currentColor">
<path
d="m237.66 85.26l-128.4 128.4a8 8 0 0 1-11.32 0l-71.6-72a8 8 0 0 1 0-11.31l24-24a8 8 0 0 1 11.32 0l36.68 35.32a8 8 0 0 0 11.32 0l92.68-91.32a8 8 0 0 1 11.32 0l24 23.6a8 8 0 0 1 0 11.31"
opacity=".2" />
<path
d="m243.28 68.24l-24-23.56a16 16 0 0 0-22.58 0L104 136l-.11-.11l-36.64-35.27a16 16 0 0 0-22.57.06l-24 24a16 16 0 0 0 0 22.61l71.62 72a16 16 0 0 0 22.63 0l128.4-128.38a16 16 0 0 0-.05-22.67M103.62 208L32 136l24-24l.11.11l36.64 35.27a16 16 0 0 0 22.52 0L208.06 56L232 79.6Z" />
</g>
</svg></div>
@else
@if ($error)
<div class="flex w-64 gap-2">Docker is installed: <svg class="w-5 h-5 text-error" viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M208.49 191.51a12 12 0 0 1-17 17L128 145l-63.51 63.49a12 12 0 0 1-17-17L111 128L47.51 64.49a12 12 0 0 1 17-17L128 111l63.51-63.52a12 12 0 0 1 17 17L145 128Z" />
</svg></div>
@else
<div class="w-64"><x-loading text="Docker is installed:" /></div>
@endif
@endif
@endif
@isset($docker_version)
<div class="flex w-64 gap-2">Minimum Docker version installed: <svg class="w-5 h-5 text-success"
viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<g fill="currentColor">
<path
d="m237.66 85.26l-128.4 128.4a8 8 0 0 1-11.32 0l-71.6-72a8 8 0 0 1 0-11.31l24-24a8 8 0 0 1 11.32 0l36.68 35.32a8 8 0 0 0 11.32 0l92.68-91.32a8 8 0 0 1 11.32 0l24 23.6a8 8 0 0 1 0 11.31"
opacity=".2" />
<path
d="m243.28 68.24l-24-23.56a16 16 0 0 0-22.58 0L104 136l-.11-.11l-36.64-35.27a16 16 0 0 0-22.57.06l-24 24a16 16 0 0 0 0 22.61l71.62 72a16 16 0 0 0 22.63 0l128.4-128.38a16 16 0 0 0-.05-22.67M103.62 208L32 136l24-24l.11.11l36.64 35.27a16 16 0 0 0 22.52 0L208.06 56L232 79.6Z" />
</g>
</svg></div>
@endisset
<livewire:new-activity-monitor header="Logs" />
@isset($error)
<pre class="font-bold whitespace-pre-line text-error">{!! $error !!}</pre>
@endisset
</div>

View File

@@ -1,11 +1,14 @@
<div>
<h1>Tags</h1>
<div>Here you can see all the tags here</div>
<div class="flex gap-2 pt-10">
@forelse ($tags as $tag)
<a class="box" href="{{ route('tags.show', ['tag_name' => $tag->name]) }}">{{ $tag->name }}</a>
@empty
<div>No tags yet defined yet. Go to a resource and add a tag there.</div>
@endforelse
<div class="flex flex-col gap-2 pb-6 ">
<div>Available tags: </div>
<div class="flex flex-wrap gap-2">
@forelse ($tags as $oneTag)
<a class="flex items-center justify-center h-6 px-2 text-white min-w-14 w-fit hover:no-underline hover:bg-coolgray-200 bg-coolgray-100"
href="{{ route('tags.show', ['tag_name' => $oneTag->name]) }}">{{ $oneTag->name }}</a>
@empty
<div>No tags yet defined yet. Go to a resource and add a tag there.</div>
@endforelse
</div>
</div>
</div>

View File

@@ -1,24 +1,46 @@
<div>
<div class="flex items-start gap-2">
<div>
<h1>Tag: {{ $tag->name }}</h1>
<div class="pt-2">Tag details</div>
<h1>Tags</h1>
</div>
</div>
<div class="pt-4">
<div class="flex flex-col gap-2 pb-6 ">
<div>Available tags: </div>
<div class="flex flex-wrap gap-2 ">
@forelse ($tags as $oneTag)
<a :class="{{ $tag->id == $oneTag->id }} && 'bg-coollabs hover:bg-coollabs-100'"
class="flex items-center justify-center h-6 px-2 text-white min-w-14 w-fit hover:no-underline hover:bg-coolgray-200 bg-coolgray-100"
href="{{ route('tags.show', ['tag_name' => $oneTag->name]) }}">{{ $oneTag->name }}</a>
@empty
<div>No tags yet defined yet. Go to a resource and add a tag there.</div>
@endforelse
</div>
</div>
<div>
<h3 class="py-4">Details</h3>
<div class="flex items-end gap-2 ">
<div class="w-[500px]">
<x-forms.input readonly label="Deploy Webhook URL" id="webhook" />
</div>
<x-new-modal buttonTitle="Redeploy All" action="redeploy_all" class="mt-1">
<x-new-modal isHighlighted buttonTitle="Redeploy All" action="redeploy_all">
All resources will be redeployed.
</x-new-modal>
</div>
<div class="grid gap-2 pt-4 lg:grid-cols-4">
@foreach ($resources as $resource)
<a href="{{ $resource->link() }}" class="flex flex-col box group">
<span class="font-bold text-white">{{ $resource->name }}</span>
<span class="description">{{ $resource->description }}</span>
<div class="grid grid-cols-1 gap-2 pt-4 lg:grid-cols-2 xl:grid-cols-3">
@foreach ($applications as $application)
<a href="{{ $application->link() }}" class="flex flex-col box group">
<span
class="font-bold text-white">{{ $application->project()->name }}/{{ $application->environment->name }}</span>
<span class="text-white ">{{ $application->name }}</span>
<span class="description">{{ $application->description }}</span>
</a>
@endforeach
@foreach ($services as $service)
<a href="{{ $service->link() }}" class="flex flex-col box group">
<span
class="font-bold text-white">{{ $service->project()->name }}/{{ $service->environment->name }}</span>
<span class="text-white ">{{ $service->name }}</span>
<span class="description">{{ $service->description }}</span>
</a>
@endforeach
</div>

View File

@@ -824,8 +824,12 @@ Route::post('/payments/stripe/events', function () {
if (!$team) {
throw new Exception('No team found for subscription: ' . $subscription->id);
}
SubscriptionInvoiceFailedJob::dispatch($team);
send_internal_notification('Invoice payment failed: ' . $subscription->team->id);
if (!$subscription->stripe_invoice_paid) {
SubscriptionInvoiceFailedJob::dispatch($team);
send_internal_notification('Invoice payment failed: ' . $subscription->team->id);
} else {
send_internal_notification('Invoice payment failed but already paid: ' . $subscription->team->id);
}
break;
case 'payment_intent.payment_failed':
$customerId = data_get($data, 'customer');

View File

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