update boarding flow

This commit is contained in:
Andras Bacsai
2023-08-23 10:14:39 +02:00
parent b39ca51d41
commit d62af76097
17 changed files with 214 additions and 106 deletions

View File

@@ -20,3 +20,4 @@ yarn-error.log
/.npm /.npm
/.bash_history /.bash_history
/_data /_data
.rnd

1
.gitignore vendored
View File

@@ -29,3 +29,4 @@ _ide_helper.php
.gitignore .gitignore
.phpstorm.meta.php .phpstorm.meta.php
_ide_helper_models.php _ide_helper_models.php
.rnd

View File

@@ -12,6 +12,14 @@ class InstallDocker
{ {
$dockerVersion = '23.0'; $dockerVersion = '23.0';
$config = base64_encode('{ "live-restore": true }'); $config = base64_encode('{ "live-restore": true }');
if (is_dev()) {
$activity = remote_process([
"echo ####### Installing Prerequisites...",
"echo ####### Installing/updating Docker Engine...",
"echo ####### Configuring Docker Engine (merging existing configuration with the required)...",
"echo ####### Restarting Docker Engine...",
], $server);
} else {
$activity = remote_process([ $activity = remote_process([
"echo ####### Installing Prerequisites...", "echo ####### Installing Prerequisites...",
"command -v jq >/dev/null || apt-get update", "command -v jq >/dev/null || apt-get update",
@@ -33,8 +41,9 @@ class InstallDocker
'name' => 'coolify', 'name' => 'coolify',
'network' => 'coolify', 'network' => 'coolify',
'server_id' => $server->id, 'server_id' => $server->id,
'team_id' => $team->id
]); ]);
}
return $activity; return $activity;
} }

View File

@@ -2,6 +2,7 @@
namespace App\Http\Livewire; namespace App\Http\Livewire;
use App\Actions\Server\InstallDocker;
use App\Models\PrivateKey; use App\Models\PrivateKey;
use App\Models\Project; use App\Models\Project;
use App\Models\Server; use App\Models\Server;
@@ -9,9 +10,7 @@ use Livewire\Component;
class Boarding extends Component class Boarding extends Component
{ {
public string $currentState = 'welcome';
public string $currentState = 'create-private-key';
// public ?string $serverType = null;
public ?string $privateKeyType = null; public ?string $privateKeyType = null;
public ?string $privateKey = null; public ?string $privateKey = null;
@@ -26,6 +25,8 @@ class Boarding extends Component
public ?string $remoteServerUser = 'root'; public ?string $remoteServerUser = 'root';
public ?Server $createdServer = null; public ?Server $createdServer = null;
public ?Project $createdProject = null;
public function mount() public function mount()
{ {
$this->privateKeyName = generate_random_name(); $this->privateKeyName = generate_random_name();
@@ -64,7 +65,11 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function setServer(string $type) public function setServer(string $type)
{ {
if ($type === 'localhost') { if ($type === 'localhost') {
$this->currentState = 'create-project'; $this->createdServer = Server::find(0);
if (!$this->createdServer) {
return $this->emit('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.');
}
$this->currentState = 'select-proxy';
} elseif ($type === 'remote') { } elseif ($type === 'remote') {
$this->currentState = 'private-key'; $this->currentState = 'private-key';
} }
@@ -126,22 +131,50 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->currentState = 'install-docker'; $this->currentState = 'install-docker';
return; return;
} }
ray($uptime, $dockerVersion);
} catch (\Exception $e) { } catch (\Exception $e) {
return general_error_handler(customErrorMessage: "Server is not reachable. Reason: {$e->getMessage()}", that: $this); return general_error_handler(customErrorMessage: "Server is not reachable. Reason: {$e->getMessage()}", that: $this);
} }
} }
public function installDocker()
{
$activity = resolve(InstallDocker::class)($this->createdServer, currentTeam());
$this->emit('newMonitorActivity', $activity->id);
$this->currentState = 'select-proxy';
}
public function selectProxy(string|null $proxyType = null)
{
if (!$proxyType) {
return $this->currentState = 'create-project';
}
$this->createdServer->proxy->type = $proxyType;
$this->createdServer->proxy->status = 'exited';
$this->createdServer->save();
$this->currentState = 'create-project';
}
public function createNewProject()
{
$this->createdProject = Project::create([
'name' => generate_random_name(),
'team_id' => currentTeam()->id
]);
$this->currentState = 'create-resource';
}
public function showNewResource()
{
$this->skipBoarding();
return redirect()->route(
'project.resources.new',
[
'project_uuid' => $this->createdProject->uuid,
'environment_name' => 'production',
]
);
}
private function createNewPrivateKey() private function createNewPrivateKey()
{ {
$this->privateKeyName = generate_random_name(); $this->privateKeyName = generate_random_name();
$this->privateKeyDescription = 'Created by Coolify'; $this->privateKeyDescription = 'Created by Coolify';
$this->privateKey = generateSSHKey(); ['private' => $this->privateKey] = generateSSHKey();
}
public function createNewProject()
{
Project::create([
'name' => generate_random_name(),
'team_id' => currentTeam()->id
]);
} }
} }

View File

@@ -60,20 +60,19 @@ class StandaloneDocker extends Component
$found = $this->server->standaloneDockers()->where('network', $this->network)->first(); $found = $this->server->standaloneDockers()->where('network', $this->network)->first();
if ($found) { if ($found) {
$this->createNetworkAndAttachToProxy(); $this->createNetworkAndAttachToProxy();
$this->addError('network', 'Network already added to this server.'); $this->emit('error', 'Network already added to this server.');
return; return;
} else { } else {
$docker = ModelsStandaloneDocker::create([ $docker = ModelsStandaloneDocker::create([
'name' => $this->name, 'name' => $this->name,
'network' => $this->network, 'network' => $this->network,
'server_id' => $this->server_id, 'server_id' => $this->server_id,
'team_id' => currentTeam()->id
]); ]);
} }
$this->createNetworkAndAttachToProxy(); $this->createNetworkAndAttachToProxy();
return redirect()->route('destination.show', $docker->uuid); return redirect()->route('destination.show', $docker->uuid);
} catch (\Exception $e) { } catch (\Exception $e) {
return general_error_handler(err: $e); return general_error_handler(err: $e, that: $this);
} }
} }

View File

@@ -3,6 +3,7 @@
namespace App\Http\Livewire\Project\New; namespace App\Http\Livewire\Project\New;
use App\Models\Server; use App\Models\Server;
use Countable;
use Livewire\Component; use Livewire\Component;
class Select extends Component class Select extends Component
@@ -11,7 +12,7 @@ class Select extends Component
public string $type; public string $type;
public string $server_id; public string $server_id;
public string $destination_uuid; public string $destination_uuid;
public $servers = []; public Countable|array|Server $servers;
public $destinations = []; public $destinations = [];
public array $parameters; public array $parameters;
@@ -23,6 +24,13 @@ class Select extends Component
public function set_type(string $type) public function set_type(string $type)
{ {
$this->type = $type; $this->type = $type;
if (count($this->servers) === 1) {
$server = $this->servers->first();
$this->set_server($server);
if (count($server->destinations()) === 1) {
$this->set_destination($server->destinations()->first()->uuid);
}
}
$this->current_step = 'servers'; $this->current_step = 'servers';
} }

View File

@@ -35,7 +35,7 @@ class Proxy extends Component
$this->emit('proxyStatusUpdated'); $this->emit('proxyStatusUpdated');
} }
public function select_proxy(string $proxy_type) public function select_proxy(ProxyTypes $proxy_type)
{ {
$this->server->proxy->type = $proxy_type; $this->server->proxy->type = $proxy_type;
$this->server->proxy->status = 'exited'; $this->server->proxy->status = 'exited';

View File

@@ -10,21 +10,30 @@ class Server extends BaseModel
{ {
use SchemalessAttributesTrait; use SchemalessAttributesTrait;
protected static function booted()
{
static::created(function ($server) {
ServerSetting::create([
'server_id' => $server->id,
]);
StandaloneDocker::create([
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $server->id,
]);
});
static::deleting(function ($server) {
$server->settings()->delete();
});
}
public $casts = [ public $casts = [
'proxy' => SchemalessAttributes::class, 'proxy' => SchemalessAttributes::class,
]; ];
protected $schemalessAttributes = [ protected $schemalessAttributes = [
'proxy', 'proxy',
]; ];
protected $fillable = [ protected $guarded = [];
'name',
'ip',
'user',
'port',
'team_id',
'private_key_id',
'proxy',
];
static public function isReachable() static public function isReachable()
{ {
@@ -51,17 +60,7 @@ class Server extends BaseModel
return $standaloneDocker->concat($swarmDocker); return $standaloneDocker->concat($swarmDocker);
} }
protected static function booted()
{
static::created(function ($server) {
ServerSetting::create([
'server_id' => $server->id,
]);
});
static::deleting(function ($server) {
$server->settings()->delete();
});
}
public function settings() public function settings()
{ {

View File

@@ -53,7 +53,7 @@ a {
@apply text-white; @apply text-white;
} }
.box { .box {
@apply flex items-center justify-center p-2 transition-colors rounded cursor-pointer min-h-12 bg-coolgray-200 hover:bg-coollabs-100 hover:text-white hover:no-underline; @apply flex items-center p-2 transition-colors cursor-pointer min-h-16 bg-coolgray-200 hover:bg-coollabs-100 hover:text-white hover:no-underline min-w-[24rem];
} }
.lds-heart { .lds-heart {

View File

@@ -53,12 +53,12 @@
<span v-if="search"><span class="capitalize ">{{ <span v-if="search"><span class="capitalize ">{{
sequenceState.sequence[sequenceState.currentActionIndex] }}</span> name sequenceState.sequence[sequenceState.currentActionIndex] }}</span> name
will be: will be:
<x-highlighted text="{{ search }}" /> <span class="inline-block text-warning">{{ search }}</span>
</span> </span>
<span v-else><span class="capitalize ">{{ <span v-else><span class="capitalize ">{{
sequenceState.sequence[sequenceState.currentActionIndex] }}</span> name sequenceState.sequence[sequenceState.currentActionIndex] }}</span> name
will be: will be:
<x-highlighted text="randomly generated (type to change)" /> <span class="inline-block text-warning">randomly generated (type to change)</span>
</span> </span>
</span> </span>
</li> </li>

View File

@@ -1,3 +1,13 @@
<x-layout-simple> <x-layout-simple>
<livewire:boarding /> <livewire:boarding />
<x-modal modalId="installDocker">
<x-slot:modalBody>
<livewire:activity-monitor header="Installing Docker 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-layout-simple> </x-layout-simple>

View File

@@ -1,5 +1,5 @@
<div class="grid grid-cols-1 gap-4 md:grid-cols-3"> <div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<div class="box-border col-span-2 min-w-[24rem]"> <div class="box-border col-span-2 min-w-[24rem] min-h-[21rem]">
<h1 class="text-5xl font-bold">{{$title}}</h1> <h1 class="text-5xl font-bold">{{$title}}</h1>
<div class="py-6 "> <div class="py-6 ">
@isset($question) @isset($question)
@@ -9,7 +9,7 @@
@endisset @endisset
</div> </div>
@if($actions) @if($actions)
<div class="flex flex-col gap-4 md:flex-row"> <div class="flex flex-col flex-wrap gap-4 md:flex-row">
{{$actions}} {{$actions}}
</div> </div>
@endif @endif

View File

@@ -1,3 +1,4 @@
@php use App\Enums\ProxyTypes; @endphp
<div class="min-h-screen hero"> <div class="min-h-screen hero">
<div class="hero-content"> <div class="hero-content">
<div> <div>
@@ -5,7 +6,7 @@
<h1 class="text-5xl font-bold">Welcome to Coolify</h1> <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> <p class="py-6 text-xl text-center">Let me help you to set the basics.</p>
<div class="flex justify-center "> <div class="flex justify-center ">
<div class="w-72 box" wire:click="$set('currentState', 'select-server')">Get Started <div class="justify-center box" wire:click="$set('currentState', 'select-server')">Get Started
</div> </div>
</div> </div>
@endif @endif
@@ -16,9 +17,9 @@
or on a <x-highlighted text="Remote Server" />? or on a <x-highlighted text="Remote Server" />?
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<div class="md:w-72 box" wire:click="setServer('localhost')">Localhost <div class="justify-center box" wire:click="setServer('localhost')">Localhost
</div> </div>
<div class="md:w-72 box" wire:click="setServer('remote')">Remote Server <div class="justify-center box" wire:click="setServer('remote')">Remote Server
</div> </div>
</x-slot:actions> </x-slot:actions>
<x-slot:explanation> <x-slot:explanation>
@@ -33,21 +34,21 @@
</x-boarding-step> </x-boarding-step>
@endif @endif
@if ($currentState === 'private-key') @if ($currentState === 'private-key')
<x-boarding-step title="Private Key"> <x-boarding-step title="SSH Key">
<x-slot:question> <x-slot:question>
Do you have your own Private Key? Do you have your own SSH Private Key?
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<div class="md:w-72 box" wire:click="setPrivateKey('own')">Yes <div class="justify-center box" wire:click="setPrivateKey('own')">Yes
</div> </div>
<div class="md:w-72 box" wire:click="setPrivateKey('create')">No (create one for me) <div class="justify-center box" wire:click="setPrivateKey('create')">No (create one for me)
</div> </div>
</x-slot:actions> </x-slot:actions>
<x-slot:explanation> <x-slot:explanation>
<p>Private Keys are used to connect to a remote server through a secure shell, called SSH.</p> <p>SSH Keys are used to connect to a remote server through a secure shell, called SSH.</p>
<p>You can use your own private key, or you can let Coolify to create one for you.</p> <p>You can use your own ssh private key, or you can let Coolify to create one for you.</p>
<p>In both ways, you need to add the public version of your private key to the remote server's <p>In both ways, you need to add the public version of your ssh private key to the remote server's
<code>~/.ssh/authorized_keys</code> file. <code class="text-warning">~/.ssh/authorized_keys</code> file.
</p> </p>
</x-slot:explanation> </x-slot:explanation>
</x-boarding-step> </x-boarding-step>
@@ -114,10 +115,38 @@
Could not find Docker Engine on your server. Do you want me to install it for you? Could not find Docker Engine on your server. Do you want me to install it for you?
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<div class="w-72 box" wire:click="installDocker">Let's do it!</div> <div class="justify-center box" wire:click="installDocker" onclick="installDocker.showModal()">Let's do
it!</div>
</x-slot:actions> </x-slot:actions>
<x-slot:explanation> <x-slot:explanation>
<p>This will install the latest Docker Engine on your server, configure a few things to be able to run optimal.</p> <p>This will install the latest Docker Engine on your server, configure a few things to be able
to run optimal.</p>
</x-slot:explanation>
</x-boarding-step>
@endif
@if ($currentState === 'select-proxy')
<x-boarding-step title="Select a Proxy">
<x-slot:question>
If you would like to attach any kind of domain to your resources, you need a proxy.
</x-slot:question>
<x-slot:actions>
<x-forms.button wire:click="selectProxy" class="w-64 box">
Decide later
</x-forms.button>
<x-forms.button class="w-32 box" wire:click="selectProxy('{{ ProxyTypes::TRAEFIK_V2 }}')">
Traefik
v2
</x-forms.button>
<x-forms.button disabled class="w-32 box">
Nginx
</x-forms.button>
<x-forms.button disabled class="w-32 box">
Caddy
</x-forms.button>
</x-slot:actions>
<x-slot:explanation>
<p>This will install the latest Docker Engine on your server, configure a few things to be able
to run optimal.</p>
</x-slot:explanation> </x-slot:explanation>
</x-boarding-step> </x-boarding-step>
@endif @endif
@@ -127,7 +156,7 @@
I will create an initial project for you. You can change all the details later on. I will create an initial project for you. You can change all the details later on.
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<div class="w-72 box" wire:click="createNewProject">Let's do it!</div> <div class="justify-center box" wire:click="createNewProject">Let's do it!</div>
</x-slot:actions> </x-slot:actions>
<x-slot:explanation> <x-slot:explanation>
<p>Projects are bound together several resources into one virtual group. There are no <p>Projects are bound together several resources into one virtual group. There are no
@@ -137,6 +166,20 @@
</x-slot:explanation> </x-slot:explanation>
</x-boarding-step> </x-boarding-step>
@endif @endif
@if ($currentState === 'create-resource')
<x-boarding-step title="Resources">
<x-slot:question>
I will redirect you to the new resource page, where you can create your first resource.
</x-slot:question>
<x-slot:actions>
<div class="justify-center box" wire:click="showNewResource">Let's do
it!</div>
</x-slot:actions>
<x-slot:explanation>
<p>A resource could be an application, a database or a service (like WordPress).</p>
</x-slot:explanation>
</x-boarding-step>
@endif
<div class="flex justify-center gap-2 pt-4"> <div class="flex justify-center gap-2 pt-4">
<a wire:click='skipBoarding'>Skip boarding process</a> <a wire:click='skipBoarding'>Skip boarding process</a>
<a wire:click='restartBoarding'>Restart boarding process</a> <a wire:click='restartBoarding'>Restart boarding process</a>

View File

@@ -18,18 +18,23 @@
<div class="">N/A</div> <div class="">N/A</div>
@endforelse @endforelse
</div> </div>
<div class="grid gap-2 pt-2"> <div class="pt-2">
@if (count($networks) > 0) @if (count($networks) > 0)
<h4>Found Destinations</h4> <h3 class="pb-4">Found Destinations</h3>
@endif @endif
<div class="flex flex-wrap gap-2 ">
@foreach ($networks as $network) @foreach ($networks as $network)
<div >
<a <a
href="{{ route('destination.new', ['server_id' => $server->id, 'network_name' => data_get($network, 'Name')]) }}"> href="{{ route('destination.new', ['server_id' => $server->id, 'network_name' => data_get($network, 'Name')]) }}">
<x-forms.button>+<x-highlighted text="{{ data_get($network, 'Name') }}" /> <x-forms.button >+<x-highlighted text="{{ data_get($network, 'Name') }}" />
</x-forms.button> </x-forms.button>
</a> </a>
</div>
@endforeach @endforeach
</div> </div>
</div>
@else @else
<div>Server is not validated. Validate first.</div> <div>Server is not validated. Validate first.</div>
@endif @endif

View File

@@ -3,9 +3,6 @@
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<h1>Project: {{ data_get($project, 'name') }}</h1> <h1>Project: {{ data_get($project, 'name') }}</h1>
<x-forms.button type="submit">Save</x-forms.button> <x-forms.button type="submit">Save</x-forms.button>
@if ($project->applications->count() === 0)
<livewire:project.delete-project :project_id="$project->id" />
@endif
</div> </div>
<div class="pb-10">Edit project details here.</div> <div class="pb-10">Edit project details here.</div>
<div class="flex gap-2"> <div class="flex gap-2">

View File

@@ -10,7 +10,7 @@
</ul> </ul>
<h2>Applications</h2> <h2>Applications</h2>
<div class="grid justify-start grid-cols-1 gap-2 text-left xl:grid-cols-3"> <div class="grid justify-start grid-cols-1 gap-2 text-left xl:grid-cols-3">
<div class="gap-2 py-4 cursor-pointer group hover:bg-coollabs bg-coolgray-200 min-w-[24rem]" <div class="box group"
wire:click="set_type('public')"> wire:click="set_type('public')">
<div class="flex flex-col mx-6"> <div class="flex flex-col mx-6">
<div class="group-hover:text-white"> <div class="group-hover:text-white">
@@ -21,7 +21,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="gap-2 py-4 cursor-pointer group hover:bg-coollabs bg-coolgray-200 min-w-[24rem]" <div class="box group"
wire:click="set_type('private-gh-app')"> wire:click="set_type('private-gh-app')">
<div class="flex flex-col mx-6"> <div class="flex flex-col mx-6">
<div class="group-hover:text-white"> <div class="group-hover:text-white">
@@ -32,7 +32,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="gap-2 py-4 cursor-pointer group hover:bg-coollabs bg-coolgray-200 min-w-[24rem]" <div class="box group"
wire:click="set_type('private-deploy-key')"> wire:click="set_type('private-deploy-key')">
<div class="flex flex-col mx-6"> <div class="flex flex-col mx-6">
<div class="group-hover:text-white"> <div class="group-hover:text-white">
@@ -45,7 +45,7 @@
</div> </div>
</div> </div>
<div class="grid justify-start grid-cols-1 gap-2 text-left xl:grid-cols-3"> <div class="grid justify-start grid-cols-1 gap-2 text-left xl:grid-cols-3">
<div class="gap-2 py-4 cursor-pointer group hover:bg-coollabs bg-coolgray-200 min-w-[24rem]" <div class="box group"
wire:click="set_type('dockerfile')"> wire:click="set_type('dockerfile')">
<div class="flex flex-col mx-6"> <div class="flex flex-col mx-6">
<div class="group-hover:text-white"> <div class="group-hover:text-white">
@@ -59,7 +59,7 @@
</div> </div>
<h2 class="py-4">Databases</h2> <h2 class="py-4">Databases</h2>
<div class="flex flex-col justify-start gap-2 text-left xl:flex-row"> <div class="flex flex-col justify-start gap-2 text-left xl:flex-row">
<div class="gap-2 py-4 cursor-pointer group hover:bg-coollabs bg-coolgray-200" <div class="box group"
wire:click="set_type('postgresql')"> wire:click="set_type('postgresql')">
<div class="flex flex-col mx-6"> <div class="flex flex-col mx-6">
<div class="group-hover:text-white"> <div class="group-hover:text-white">
@@ -79,9 +79,9 @@
<li class="step step-secondary">Select a Server</li> <li class="step step-secondary">Select a Server</li>
<li class="step">Select a Destination</li> <li class="step">Select a Destination</li>
</ul> </ul>
<div class="flex flex-col justify-center gap-2 text-left xl:flex-row"> <div class="grid grid-cols-3 gap-2 text-left ">
@forelse($servers as $server) @forelse($servers as $server)
<div class="gap-2 py-4 cursor-pointer group hover:bg-coollabs bg-coolgray-200" <div class="box group"
wire:click="set_server({{ $server }})"> wire:click="set_server({{ $server }})">
<div class="flex flex-col mx-6"> <div class="flex flex-col mx-6">
<div class="group-hover:text-white"> <div class="group-hover:text-white">
@@ -108,9 +108,9 @@
<li class="step step-secondary">Select a Server</li> <li class="step step-secondary">Select a Server</li>
<li class="step step-secondary">Select a Destination</li> <li class="step step-secondary">Select a Destination</li>
</ul> </ul>
<div class="flex flex-col justify-center gap-2 text-left xl:flex-row"> <div class="grid grid-cols-3 gap-2 text-left ">
@foreach ($destinations as $destination) @foreach ($destinations as $destination)
<div class="gap-2 py-4 cursor-pointer group hover:bg-coollabs bg-coolgray-200" <div class="box group"
wire:click="set_destination('{{ $destination->uuid }}')"> wire:click="set_destination('{{ $destination->uuid }}')">
<div class="flex flex-col mx-6"> <div class="flex flex-col mx-6">
<div class="group-hover:text-white"> <div class="group-hover:text-white">

View File

@@ -3,8 +3,11 @@
<h1>Environments</h1> <h1>Environments</h1>
<x-forms.button class="btn" onclick="newEnvironment.showModal()">+ Add</x-forms.button> <x-forms.button class="btn" onclick="newEnvironment.showModal()">+ Add</x-forms.button>
<livewire:project.add-environment :project="$project" /> <livewire:project.add-environment :project="$project" />
@if ($project->applications->count() === 0)
<livewire:project.delete-project :project_id="$project->id" />
@endif
</div> </div>
<div class="subtitle text-xs truncate lg:text-sm">{{ $project->name }}</div> <div class="text-xs truncate subtitle lg:text-sm">{{ $project->name }}</div>
<div class="grid gap-2 lg:grid-cols-2"> <div class="grid gap-2 lg:grid-cols-2">
@forelse ($project->environments as $environment) @forelse ($project->environments as $environment)
<a class="box" href="{{ route('project.resources', [$project->uuid, $environment->name]) }}"> <a class="box" href="{{ route('project.resources', [$project->uuid, $environment->name]) }}">