feat: init postgresql database

This commit is contained in:
Andras Bacsai 2023-08-07 18:46:40 +02:00
parent 0a040a0531
commit a020bc872d
38 changed files with 430 additions and 66 deletions

View File

@ -0,0 +1,25 @@
<?php
namespace App\Actions\Database;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\Team;
use App\Models\Postgresql;
class StartPostgresql
{
public function __invoke(Server $server, Postgresql $database)
{
$activity = remote_process([
"echo 'Creating required Docker networks...'",
"echo 'Creating required Docker networks...'",
"echo 'Creating required Docker networks...'",
"sleep 4",
"echo 'Creating required Docker networks...'",
"echo 'Creating required Docker networks...'",
], $server);
return $activity;
}
}

View File

@ -84,4 +84,4 @@ public function deployment()
'deployment_uuid' => $deploymentUuid,
]);
}
}
}

View File

@ -43,7 +43,9 @@ public function dashboard()
$s3s = S3Storage::ownedByCurrentTeam()->get();
$resources = 0;
foreach ($projects as $project) {
ray($project->postgresqls);
$resources += $project->applications->count();
$resources += $project->postgresqls->count();
}
return view('dashboard', [

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers;
use App\Models\ApplicationDeploymentQueue;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Request;
use Spatie\Activitylog\Models\Activity;
class DatabaseController extends Controller
{
use AuthorizesRequests, ValidatesRequests;
public function configuration()
{
$project = session('currentTeam')->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (!$environment) {
return redirect()->route('dashboard');
}
$database = $environment->databases->where('uuid', request()->route('database_uuid'))->first();
if (!$database) {
return redirect()->route('dashboard');
}
return view('project.database.configuration', ['database' => $database]);
}
}

View File

@ -51,4 +51,4 @@ protected function setStatus($status)
]);
$this->activity->save();
}
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Livewire;
namespace App\Http\Livewire\Dev;
use App\Models\S3Storage;
use Illuminate\Support\Facades\Storage;
@ -14,7 +14,6 @@ class S3Test extends Component
public $file;
public function mount() {
$this->s3 = S3Storage::first();
ray($this->s3);
}
public function save() {
try {

View File

@ -33,6 +33,7 @@ class General extends Component
protected $rules = [
'application.name' => 'required',
'application.description' => 'nullable',
'application.fqdn' => 'nullable',
'application.git_repository' => 'required',
'application.git_branch' => 'required',
@ -49,6 +50,7 @@ class General extends Component
];
protected $validationAttributes = [
'application.name' => 'name',
'application.description' => 'description',
'application.fqdn' => 'FQDN',
'application.git_repository' => 'Git repository',
'application.git_branch' => 'Git branch',

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Livewire\Application;
namespace App\Http\Livewire\Project\Application;
use App\Jobs\ApplicationContainerStatusJob;
use App\Models\Application;

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Livewire\Project\Database;
use Livewire\Component;
use App\Actions\Database\StartPostgresql;
class Heading extends Component
{
public $database;
public array $parameters;
public function mount()
{
$this->parameters = getRouteParameters();
}
public function start() {
if ($this->database->type() === 'postgresql') {
$activity = resolve(StartPostgresql::class)($this->database->destination->server, $this->database);
$this->emit('newMonitorActivity', $activity->id);
}
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Http\Livewire\Project\Database\Postgresql;
use Livewire\Component;
class General extends Component
{
public $database;
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.postgres_user' => 'required',
'database.postgres_password' => 'required',
'database.postgres_db' => 'required',
'database.postgres_initdb_args' => 'nullable',
'database.postgres_host_auth_method' => 'nullable',
'database.init_scripts' => 'nullable',
];
protected $validationAttributes = [
'database.name' => 'Name',
'database.description' => 'Description',
'database.postgres_user' => 'Postgres User',
'database.postgres_password' => 'Postgres Password',
'database.postgres_db' => 'Postgres DB',
'database.postgres_initdb_args' => 'Postgres Initdb Args',
'database.postgres_host_auth_method' => 'Postgres Host Auth Method',
'database.init_scripts' => 'Init Scripts',
];
public function submit() {
try {
$this->validate();
$this->database->save();
$this->emit('success', 'Database updated successfully.');
} catch (\Exception $e) {
return general_error_handler(err: $e, that: $this);
}
}
}

View File

@ -17,6 +17,12 @@ protected function name(): Attribute
set: fn (string $value) => strtolower($value),
);
}
public function can_delete_environment() {
return $this->applications()->count() == 0 && $this->postgresqls()->count() == 0;
}
public function databases() {
return $this->postgresqls();
}
public function project()
{
return $this->belongsTo(Project::class);
@ -25,12 +31,12 @@ public function applications()
{
return $this->hasMany(Application::class);
}
public function databases()
public function postgresqls()
{
return $this->hasMany(Database::class);
return $this->hasMany(Postgresql::class);
}
public function services()
{
return $this->hasMany(Service::class);
}
}
}

25
app/Models/Postgresql.php Normal file
View File

@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Postgresql extends BaseModel
{
use HasFactory;
protected $guarded = [];
protected $casts = [
'postgres_password' => 'encrypted',
];
public function type() {
return 'postgresql';
}
public function environment()
{
return $this->belongsTo(Environment::class);
}
public function destination()
{
return $this->morphTo();
}
}

View File

@ -46,4 +46,8 @@ public function applications()
{
return $this->hasManyThrough(Application::class, Environment::class);
}
}
public function postgresqls()
{
return $this->hasManyThrough(Postgresql::class, Environment::class);
}
}

View File

@ -8,6 +8,10 @@ class S3Storage extends BaseModel
{
use HasFactory;
protected $guarded = [];
protected $casts = [
'key' => 'encrypted',
'secret' => 'encrypted',
];
static public function ownedByCurrentTeam(array $select = ['*'])
{

View File

@ -13,9 +13,9 @@ public function applications()
{
return $this->morphMany(Application::class, 'destination');
}
public function databases()
public function postgresqls()
{
return $this->morphMany(Database::class, 'destination');
return $this->morphMany(Postgresql::class, 'destination');
}
public function server()
{
@ -25,4 +25,4 @@ public function attachedTo()
{
return $this->applications->count() > 0 || $this->databases->count() > 0;
}
}
}

View File

@ -14,7 +14,7 @@ class Modal extends Component
*/
public function __construct(
public string $modalId,
public string $modalTitle,
public string|null $modalTitle = null,
public string|null $modalBody = null,
public string|null $modalSubmit = null,
public bool $yesOrNo = false,
@ -30,4 +30,4 @@ public function render(): View|Closure|string
{
return view('components.modal');
}
}
}

View File

@ -14,7 +14,7 @@ public function up(): void
Schema::create('s3_storages', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('name')->nullable();
$table->string('name');
$table->longText('description')->nullable();
$table->string('region')->default('us-east-1');
$table->longText('key');

View File

@ -0,0 +1,42 @@
<?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::create('postgresqls', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('name');
$table->string('description')->nullable();
$table->string('postgres_user')->default('postgres');
$table->string('postgres_password');
$table->string('postgres_db')->default('postgres');
$table->string('postgres_initdb_args')->nullable();
$table->string('postgres_host_auth_method')->nullable();
$table->json('init_scripts')->nullable();
$table->timestamp('started_at')->nullable();
$table->morphs('destination');
$table->foreignId('environment_id');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('postgresqls');
}
};

View File

@ -11,15 +11,8 @@
*/
public function up(): void
{
Schema::create('databases', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('name');
$table->morphs('destination');
$table->foreignId('environment_id');
$table->timestamps();
Schema::table('applications', function (Blueprint $table) {
$table->string('description')->nullable();
});
}
@ -28,6 +21,8 @@ public function up(): void
*/
public function down(): void
{
Schema::dropIfExists('databases');
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('description');
});
}
};
};

View File

@ -19,13 +19,11 @@ class ApplicationSeeder extends Seeder
*/
public function run(): void
{
$environment_1 = Environment::find(1);
$standalone_docker_1 = StandaloneDocker::find(1);
$github_public_source = GithubApp::where('name', 'Public GitHub')->first();
Application::create([
'name' => 'coollabsio/coolify-examples:nodejs-fastify',
'description' => 'NodeJS Fastify Example',
'fqdn' => 'http://foo.com',
'repository_project_id' => 603035348,
'git_repository' => 'coollabsio/coolify-examples',
@ -33,11 +31,11 @@ public function run(): void
'build_pack' => 'nixpacks',
'ports_exposes' => '3000',
'ports_mappings' => '3000:3000',
'environment_id' => $environment_1->id,
'destination_id' => $standalone_docker_1->id,
'environment_id' => 1,
'destination_id' => 1,
'destination_type' => StandaloneDocker::class,
'source_id' => $github_public_source->id,
'source_type' => GithubApp::class
]);
}
}
}

View File

@ -32,6 +32,7 @@ public function run(): void
EnvironmentVariableSeeder::class,
LocalPersistentVolumeSeeder::class,
S3StorageSeeder::class,
PostgresqlSeeder::class,
]);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\Postgresql;
use App\Models\StandaloneDocker;
class PostgresqlSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
Postgresql::create([
'name' => 'Local PostgreSQL',
'description' => 'Local PostgreSQL for testing',
'postgres_password' => 'postgres',
'environment_id' => 1,
'destination_id' => 1,
'destination_type' => StandaloneDocker::class,
]);
}
}

View File

@ -0,0 +1,41 @@
<div class="navbar-main">
<a class="{{ request()->routeIs('project.database.configuration') ? 'text-white' : '' }}"
href="{{ route('project.database.configuration', $parameters) }}">
<button>Configuration</button>
</a>
{{-- <x-applications.links :application="$application" /> --}}
<div class="flex-1"></div>
{{-- <x-applications.advanced :application="$application" /> --}}
@if ($database->status === 'running')
<button wire:click='start' 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="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
</path>
<path d="M7.05 11.038v-3.988"></path>
</svg>
Restart
</button>
<button wire:click='stop' 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-error" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
</svg>
Stop
</button>
@else
<button wire:click='start' onclick="logs.showModal()"
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>Start
</button>
@endif
</div>

View File

@ -10,7 +10,9 @@
</svg>
</div>
<div class="flex flex-col w-full gap-2">
<h3 class="text-lg font-bold">{{ $modalTitle }}</h3>
@isset($modalTitle)
<h3 class="text-lg font-bold">{{ $modalTitle }}</h3>
@endisset
@isset($modalBody)
{{ $modalBody }}
@endisset
@ -31,8 +33,11 @@
</div>
</form>
@else
<form method="dialog" class="flex flex-col gap-2 rounded modal-box" wire:submit.prevent='submit'>
<h3 class="text-lg font-bold">{{ $modalTitle }}</h3>
<form method="dialog" class="flex flex-col w-11/12 max-w-5xl gap-2 rounded modal-box"
wire:submit.prevent='submit'>
@isset($modalTitle)
<h3 class="text-lg font-bold">{{ $modalTitle }}</h3>
@endisset
@isset($modalBody)
{{ $modalBody }}
@endisset

View File

@ -3,7 +3,7 @@
<li class="inline-flex items-center">
<a class="text-xs truncate lg:text-sm"
href="{{ route('project.show', ['project_uuid' => $this->parameters['project_uuid']]) }}">
{{ $application->environment->project->name }}</a>
{{ $resource->environment->project->name }}</a>
</li>
<li>
<div class="flex items-center">
@ -25,7 +25,7 @@
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path>
</svg>
<span class="text-xs truncate lg:text-sm">{{ data_get($application, 'name') }}</span>
<span class="text-xs truncate lg:text-sm">{{ data_get($resource, 'name') }}</span>
</div>
</li>
<li>
@ -38,9 +38,9 @@
</svg>
</div>
</li>
@if ($application->status === 'running')
@if ($resource->status === 'running')
<x-status.running />
@elseif($application->status === 'restarting')
@elseif($resource->status === 'restarting')
<x-status.restarting />
@else
<x-status.stopped />

View File

@ -23,6 +23,6 @@
</div>
</div>
@if (isDev())
<livewire:s3-test />
<livewire:dev.s3-test />
@endif
</x-layout>

View File

@ -10,21 +10,22 @@
<div class="flex flex-col gap-2 py-4">
<div class="flex flex-col items-end gap-2 xl:flex-row">
<x-forms.input id="application.name" label="Name" required />
<x-forms.input placeholder="https://coolify.io" id="application.fqdn" label="Domains"
helper="You can specify one domain with path or more with comma.<br><span class='text-helper'>Example</span>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3" />
@if ($wildcard_domain)
<div class="flex flex-row gap-2">
@if ($global_wildcard_domain)
<x-forms.button wire:click="generateGlobalRandomDomain">Set Global Wildcard
</x-forms.button>
@endif
@if ($server_wildcard_domain)
<x-forms.button wire:click="generateServerRandomDomain">Set Server Wildcard
</x-forms.button>
@endif
</div>
@endif
<x-forms.input id="application.description" label="Description" />
</div>
<x-forms.input placeholder="https://coolify.io" id="application.fqdn" label="Domains"
helper="You can specify one domain with path or more with comma.<br><span class='text-helper'>Example</span>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3" />
@if ($wildcard_domain)
<div class="flex flex-row gap-2">
@if ($global_wildcard_domain)
<x-forms.button wire:click="generateGlobalRandomDomain">Set Global Wildcard
</x-forms.button>
@endif
@if ($server_wildcard_domain)
<x-forms.button wire:click="generateServerRandomDomain">Set Server Wildcard
</x-forms.button>
@endif
</div>
@endif
<x-forms.select id="application.build_pack" label="Build Pack" required>
<option value="nixpacks">Nixpacks</option>
<option disabled value="docker">Docker</option>

View File

@ -1,4 +1,4 @@
<nav x-init="$wire.check_status" wire:poll.10000ms="check_status">
<x-applications.breadcrumbs :application="$application" :parameters="$parameters" />
<x-resources.breadcrumbs :resource="$application" :parameters="$parameters" />
<x-applications.navbar :application="$application" :parameters="$parameters" />
</nav>

View File

@ -0,0 +1,4 @@
<nav>
<x-resources.breadcrumbs :resource="$database" :parameters="$parameters" />
<x-databases.navbar :database="$database" :parameters="$parameters" />
</nav>

View File

@ -0,0 +1,18 @@
<div>
<form wire:submit.prevent="submit">
<div class="flex items-center gap-2">
<h2>General</h2>
<x-forms.button type="submit">
Save
</x-forms.button>
</div>
<x-forms.input label="Name" id="database.name" />
<x-forms.input label="Description" id="database.description" />
<x-forms.input label="Username" id="database.postgres_username" placeholder="If empty, use postgres." />
<x-forms.input label="Password" id="database.postgres_password" type="password" />
<x-forms.input label="Database" id="database.postgres_db" placeholder="If empty, use $USERNAME." />
<x-forms.input label="Init Args" id="database.postgres_initdb_args" placeholder="If empty, use default." />
<x-forms.input label="Host Auth Method" id="database.postgres_host_auth_method"
placeholder="If empty, use default." />
</form>
</div>

View File

@ -1,6 +1,6 @@
<x-layout>
<h1>Configuration</h1>
<livewire:application.heading :application="$application" />
<livewire:project.application.heading :application="$application" />
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex h-full pt-6">
<div class="flex flex-col gap-4 min-w-fit">
<a :class="activeTab === 'general' && 'text-white'"

View File

@ -1,5 +1,5 @@
<x-layout>
<h1 class="py-0">Deployment</h1>
<livewire:application.heading :application="$application" />
<livewire:project.application.heading :application="$application" />
<livewire:project.application.deployment-logs :application_deployment_queue="$application_deployment_queue" />
</x-layout>

View File

@ -1,5 +1,5 @@
<x-layout>
<h1>Deployments</h1>
<livewire:application.heading :application="$application" />
<livewire:project.application.heading :application="$application" />
<livewire:project.application.deployments :application="$application" :deployments="$deployments" :deployments_count="$deployments_count" />
</x-layout>

View File

@ -1,5 +0,0 @@
<x-layout>
<h1>Database</h1>
</x-layout>

View File

@ -0,0 +1,65 @@
<x-layout>
<h1>Configuration</h1>
<livewire:project.database.heading :database="$database" />
<x-modal modalId="logs">
<x-slot:modalBody>
<livewire:activity-monitor :header="true" />
</x-slot:modalBody>
<x-slot:modalSubmit>
<x-forms.button onclick="logs.close()" type="submit">
Close
</x-forms.button>
</x-slot:modalSubmit>
</x-modal>
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex h-full pt-6">
<div class="flex flex-col gap-4 min-w-fit">
<a :class="activeTab === 'general' && 'text-white'"
@click.prevent="activeTab = 'general'; window.location.hash = 'general'" href="#">General</a>
<a :class="activeTab === 'environment-variables' && 'text-white'"
@click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'"
href="#">Environment
Variables</a>
<a :class="activeTab === 'source' && 'text-white'"
@click.prevent="activeTab = 'source'; window.location.hash = 'source'" href="#">Source</a>
<a :class="activeTab === 'destination' && 'text-white'"
@click.prevent="activeTab = 'destination'; window.location.hash = 'destination'"
href="#">Destination
</a>
<a :class="activeTab === 'storages' && 'text-white'"
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'" href="#">Storages
</a>
<a :class="activeTab === 'resource-limits' && 'text-white'"
@click.prevent="activeTab = 'resource-limits'; window.location.hash = 'resource-limits'"
href="#">Resource Limits
</a>
<a :class="activeTab === 'danger' && 'text-white'"
@click.prevent="activeTab = 'danger'; window.location.hash = 'danger'" href="#">Danger Zone
</a>
</div>
<div class="w-full pl-8">
<div x-cloak x-show="activeTab === 'general'" class="h-full">
@if ($database->getMorphClass() === 'App\Models\Postgresql')
<livewire:project.database.postgresql.general :database="$database" />
@endif
</div>
<div x-cloak x-show="activeTab === 'environment-variables'">
{{-- <livewire:project.application.environment-variable.all :application="$application" /> --}}
</div>
<div x-cloak x-show="activeTab === 'source'">
{{-- <livewire:project.application.source :application="$application" /> --}}
</div>
<div x-cloak x-show="activeTab === 'destination'">
{{-- <livewire:project.application.destination :destination="$application->destination" /> --}}
</div>
<div x-cloak x-show="activeTab === 'storages'">
{{-- <livewire:project.application.storages.all :application="$application" /> --}}
</div>
<div x-cloak x-show="activeTab === 'resource-limits'">
{{-- <livewire:project.application.resource-limits :application="$application" /> --}}
</div>
<div x-cloak x-show="activeTab === 'danger'">
{{-- <livewire:project.application.danger :application="$application" /> --}}
</div>
</div>
</div>
</x-layout>

View File

@ -5,7 +5,7 @@
<a href="{{ route('project.resources.new', ['project_uuid' => request()->route('project_uuid'), 'environment_name' => request()->route('environment_name')]) }} "
class="font-normal text-white normal-case border-none rounded hover:no-underline btn btn-primary btn-sm no-animation">+
Add</a>
@if ($environment->applications->count() === 0)
@if ($environment->can_delete_environment())
<livewire:project.delete-environment :environment_id="$environment->id" />
@endif
</div>
@ -31,14 +31,26 @@ class="font-normal text-white normal-case border-none rounded hover:no-underline
</ol>
</nav>
</div>
@if ($environment->applications->count() === 0)
@if ($environment->can_delete_environment())
<p>No resources found.</p>
@endif
<div class="grid gap-2 lg:grid-cols-2">
@foreach ($environment->applications->sortBy('name') as $application)
<a class="box"
href="{{ route('project.application.configuration', [$project->uuid, $environment->name, $application->uuid]) }}">
{{ $application->name }}
<div class="flex flex-col">
<div>{{ $application->name }}</div>
<div class="text-xs text-gray-400">{{ $application->description }}</div>
</div>
</a>
@endforeach
@foreach ($environment->databases->sortBy('name') as $databases)
<a class="box"
href="{{ route('project.database.configuration', [$project->uuid, $environment->name, $databases->uuid]) }}">
<div class="flex flex-col">
<div>{{ $databases->name }}</div>
<div class="text-xs text-gray-400">{{ $databases->description }}</div>
</div>
</a>
@endforeach
</div>

View File

@ -1,6 +1,7 @@
<?php
use App\Http\Controllers\ApplicationController;
use App\Http\Controllers\DatabaseController;
use App\Http\Controllers\Controller;
use App\Http\Controllers\MagicController;
use App\Http\Controllers\ProjectController;
@ -52,6 +53,7 @@
Route::get('/project/{project_uuid}/{environment_name}/new', [ProjectController::class, 'new'])->name('project.resources.new');
Route::get('/project/{project_uuid}/{environment_name}', [ProjectController::class, 'resources'])->name('project.resources');
Route::get('/project/{project_uuid}/{environment_name}/application/{application_uuid}', [ApplicationController::class, 'configuration'])->name('project.application.configuration');
Route::get('/project/{project_uuid}/{environment_name}/database/{database_uuid}', [DatabaseController::class, 'configuration'])->name('project.database.configuration');
Route::get('/project/{project_uuid}/{environment_name}/application/{application_uuid}/deployment', [ApplicationController::class, 'deployments'])->name('project.application.deployments');
Route::get(
'/project/{project_uuid}/{environment_name}/application/{application_uuid}/deployment/{deployment_uuid}',