This commit is contained in:
Andras Bacsai 2023-05-22 15:47:40 +02:00
parent 1dbd1065f9
commit a044354294
25 changed files with 332 additions and 209 deletions

View File

@ -2,13 +2,24 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Spatie\Activitylog\Models\Activity; use App\Models\Project;
class ProjectController extends Controller class ProjectController extends Controller
{ {
public function environments() public function all()
{ {
$project = session('currentTeam')->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); $team_id = session('currentTeam')->id;
$projects = Project::where('team_id', $team_id)->get();
return view('projects', ['projects' => $projects]);
}
public function show()
{
$project_uuid = request()->route('project_uuid');
$team_id = session('currentTeam')->id;
$project = Project::where('team_id', $team_id)->where('uuid', $project_uuid)->first();
if (!$project) { if (!$project) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
@ -16,7 +27,7 @@ public function environments()
if (count($project->environments) == 1) { if (count($project->environments) == 1) {
return redirect()->route('project.resources', ['project_uuid' => $project->uuid, 'environment_name' => $project->environments->first()->name]); return redirect()->route('project.resources', ['project_uuid' => $project->uuid, 'environment_name' => $project->environments->first()->name]);
} }
return view('project.environments', ['project' => $project]); return view('project.show', ['project' => $project]);
} }
public function new() public function new()

View File

@ -13,6 +13,6 @@ public function createEmptyProject()
'name' => generateRandomName(), 'name' => generateRandomName(),
'team_id' => session('currentTeam')->id, 'team_id' => session('currentTeam')->id,
]); ]);
return redirect()->route('project.environments', ['project_uuid' => $project->uuid, 'environment_name' => 'production']); return redirect()->route('project.show', ['project_uuid' => $project->uuid, 'environment_name' => 'production']);
} }
} }

View File

@ -2,7 +2,10 @@
"name": "laravel/laravel", "name": "laravel/laravel",
"type": "project", "type": "project",
"description": "The Laravel Framework.", "description": "The Laravel Framework.",
"keywords": ["framework", "laravel"], "keywords": [
"framework",
"laravel"
],
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^8.2", "php": "^8.2",

View File

@ -18,7 +18,6 @@ class UserFactory extends Factory
public function definition(): array public function definition(): array
{ {
return [ return [
'name' => fake()->name(),
'uuid' => Str::uuid(), 'uuid' => Str::uuid(),
'email' => fake()->unique()->safeEmail(), 'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(), 'email_verified_at' => now(),

View File

@ -15,7 +15,7 @@ public function up(): void
$table->id(); $table->id();
$table->string('uuid')->unique(); $table->string('uuid')->unique();
$table->boolean('is_root_user')->default(false); $table->boolean('is_root_user')->default(false);
$table->string('name'); $table->string('name')->default('Your Name Here');
$table->string('email')->unique(); $table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable(); $table->timestamp('email_verified_at')->nullable();
$table->string('password'); $table->string('password');

View File

@ -11,12 +11,10 @@ public function run(): void
{ {
User::factory()->create([ User::factory()->create([
"id" => 0, "id" => 0,
'name' => 'Root User',
'email' => 'test@example.com', 'email' => 'test@example.com',
'is_root_user' => true, 'is_root_user' => true,
]); ]);
User::factory()->create([ User::factory()->create([
'name' => 'Normal User',
'email' => 'test2@example.com', 'email' => 'test2@example.com',
]); ]);
} }

View File

@ -13,10 +13,10 @@ body {
@apply bg-coolgray-100 text-neutral-400 min-h-full antialiased; @apply bg-coolgray-100 text-neutral-400 min-h-full antialiased;
} }
input[type="checkbox"] { input[type="checkbox"] {
@apply toggle toggle-warning toggle-sm; @apply toggle toggle-warning toggle-xs rounded;
} }
input { input {
@apply input input-sm placeholder:text-neutral-700 text-white; @apply input input-sm placeholder:text-neutral-700 text-white rounded-none;
} }
input[type="text"],[type="number"],[type="email"],[type="password"] { input[type="text"],[type="number"],[type="email"],[type="password"] {
@apply read-only:opacity-40; @apply read-only:opacity-40;
@ -26,17 +26,17 @@ .label-text, label {
} }
textarea { textarea {
@apply textarea placeholder:text-neutral-700 text-white; @apply textarea placeholder:text-neutral-700 text-white rounded-none;
} }
select { select {
@apply select select-sm disabled:opacity-40 font-normal placeholder:text-neutral-700 text-white; @apply select select-sm disabled:opacity-40 font-normal placeholder:text-neutral-700 text-white rounded-none;
} }
button[type="button"] { button[type="button"] {
@apply btn btn-xs btn-ghost no-animation normal-case text-white; @apply btn btn-xs btn-ghost no-animation normal-case text-white rounded;
} }
button[type="submit"] { button[type="submit"] {
@apply btn btn-xs no-animation normal-case text-white btn-primary; @apply btn btn-xs no-animation normal-case text-white btn-primary rounded;
} }
button[isWarning] { button[isWarning] {
@apply text-error; @apply text-error;
@ -57,8 +57,20 @@ a {
@apply text-neutral-400 hover:text-white text-sm link link-hover hover:bg-transparent; @apply text-neutral-400 hover:text-white text-sm link link-hover hover:bg-transparent;
} }
main {
@apply h-full w-full min-h-screen px-32 xl:px-10 pt-4 mx-auto max-w-7xl;
}
.main-navbar {
@apply fixed top-0 left-0 min-h-screen overflow-hidden;
}
.icon {
@apply w-6 h-6;
}
.icon:hover {
@apply text-white;
}
.box { .box {
@apply flex items-center justify-center text-sm rounded cursor-pointer h-14 bg-coolgray-200 hover:bg-coollabs-100 hover:text-white p-2 hover:no-underline; @apply flex items-center justify-center text-sm rounded cursor-pointer h-14 bg-coolgray-200 hover:bg-coollabs-100 hover:text-white p-2 hover:no-underline transition-colors;
} }
.main-menu { .main-menu {
@ -66,17 +78,20 @@ .main-menu {
} }
.main-menu:after { .main-menu:after {
content: "/"; content: "/";
@apply absolute border border-dotted rounded border-neutral-600 right-0 top-0 text-warning mx-1 px-2 mt-[0.7rem]; @apply absolute border border-dotted rounded border-neutral-600 right-0 top-0 text-warning mx-1 px-2 mt-[0.3rem] text-sm;
}
.magic-badge {
@apply min-w-fit px-2 rounded text-center border border-dotted border-primary text-white text-xs;
} }
.magic-input { .magic-input {
@apply input w-96 placeholder:text-neutral-700 text-sm; @apply input input-sm w-96 placeholder:text-neutral-700 text-sm rounded-none;
} }
.magic-items { .magic-items {
@apply absolute top-16 mt-2 w-[24rem] bg-coolgray-200 rounded-xl outline outline-coolgray-500; @apply absolute top-12 mt-2 w-[24rem] bg-coolgray-200 rounded z-50;
} }
.magic-item { .magic-item {
@apply text-sm flex items-center gap-4 m-2 py-2 pl-4 cursor-pointer hover:bg-coolgray-500 text-neutral-400 hover:text-white rounded-xl transition-colors hover:shadow; @apply text-sm flex items-center gap-4 m-2 py-2 pl-4 cursor-pointer hover:bg-coolgray-500 text-neutral-400 hover:text-white transition-colors hover:shadow;
} }
.magic-item-focused { .magic-item-focused {
@apply bg-neutral-700 text-white; @apply bg-coolgray-400 text-white;
} }

View File

@ -1,5 +1,5 @@
<nav class="flex gap-4 py-2 border-b-2 border-solid border-coolgray-200"> <nav class="flex gap-4 py-2 border-b-2 border-solid border-coolgray-200">
<a <a class="{{ request()->routeIs('project.application.configuration') ? 'text-white' : '' }}"
href="{{ route('project.application.configuration', [ href="{{ route('project.application.configuration', [
'project_uuid' => Route::current()->parameters()['project_uuid'], 'project_uuid' => Route::current()->parameters()['project_uuid'],
'application_uuid' => Route::current()->parameters()['application_uuid'], 'application_uuid' => Route::current()->parameters()['application_uuid'],
@ -7,7 +7,7 @@
]) }}"> ]) }}">
Configuration Configuration
</a> </a>
<a <a class="{{ request()->routeIs('project.application.deployments') ? 'text-white' : '' }}"
href="{{ route('project.application.deployments', [ href="{{ route('project.application.deployments', [
'project_uuid' => Route::current()->parameters()['project_uuid'], 'project_uuid' => Route::current()->parameters()['project_uuid'],
'application_uuid' => Route::current()->parameters()['application_uuid'], 'application_uuid' => Route::current()->parameters()['application_uuid'],
@ -16,11 +16,14 @@
Deployments Deployments
</a> </a>
<div class="flex-1"></div> <div class="flex-1"></div>
<div class="dropdown dropdown-hover"> <div class="dropdown dropdown-bottom">
<x-inputs.button>Links <button tabindex="0"
class="flex items-center justify-center h-full text-white normal-case bg-transparent border-none rounded btn btn-xs no-animation">
Links
<x-chevron-down /> <x-chevron-down />
</x-inputs.button> </button>
<ul tabindex="0" class="p-2 font-bold text-white rounded min-w-max dropdown-content menu bg-coolgray-200"> <ul tabindex="0"
class="text-xs text-white normal-case rounded min-w-max dropdown-content menu bg-coolgray-200">
<li> <li>
<a class="text-xs" target="_blank" href="{{ $application->gitBranchLocation }}"> <a class="text-xs" target="_blank" href="{{ $application->gitBranchLocation }}">
Open on Git Open on Git
@ -48,5 +51,6 @@
@endif @endif
</ul> </ul>
</div> </div>
</div>
<livewire:project.application.deploy :applicationId="$application->id" /> <livewire:project.application.deploy :applicationId="$application->id" />
</nav> </nav>

View File

@ -25,16 +25,18 @@
</head> </head>
<body> <body>
@livewireScripts @livewireScripts
@auth @auth
<x-navbar /> <x-navbar />
@endauth @endauth
<main class="min-h-full px-8 pt-10 mx-auto max-w-7xl"> <main>
<div class="flex justify-center w-full">
<x-magic-bar />
</div>
{{ $slot }} {{ $slot }}
</main> </main>
<a <a
class="fixed text-xs cursor-pointer left-2 bottom-1 opacity-20 hover:opacity-100 hover:text-white">v{{ config('version') }}</a> class="fixed text-xs cursor-pointer right-2 bottom-1 opacity-60 hover:opacity-100 hover:text-white">v{{ config('version') }}</a>
@auth @auth
<script> <script>
window.addEventListener("keydown", function(event) { window.addEventListener("keydown", function(event) {

View File

@ -13,14 +13,10 @@
<template x-for="(item,index) in filteredItems" :key="item.name"> <template x-for="(item,index) in filteredItems" :key="item.name">
<div x-on:click="await set(item.next ?? 'server',item.name)" <div x-on:click="await set(item.next ?? 'server',item.name)"
:class="focusedIndex === index && 'magic-item-focused'" class="magic-item"> :class="focusedIndex === index && 'magic-item-focused'" class="magic-item">
<span class="w-12 badge badge-primary badge-sm" x-show="item.type === 'Apps'" <span class="magic-badge" x-show="item.type === 'Apps'" x-text="item.type"></span>
x-text="item.type"></span> <span class="magic-badge" x-show="item.type === 'Add'" x-text="item.type"></span>
<span class="w-12 badge badge-secondary badge-sm" x-show="item.type === 'Add'" <span class="magic-badge" x-show="item.type === 'Jump'" x-text="item.type"></span>
x-text="item.type"></span> <span class="magic-badge" x-show="item.type === 'New'" x-text="item.type"></span>
<span class="w-12 badge badge-success badge-sm" x-show="item.type === 'Jump'"
x-text="item.type"></span>
<span class="w-12 badge badge-success badge-sm" x-show="item.type === 'New'"
x-text="item.type"></span>
<span x-text="item.name"></span> <span x-text="item.name"></span>
</div> </div>
</template> </template>
@ -42,7 +38,7 @@
<template x-for="(server,index) in filteredServers" :key="server.name ?? server"> <template x-for="(server,index) in filteredServers" :key="server.name ?? server">
<div x-on:click="await set('destination',server.uuid)" <div x-on:click="await set('destination',server.uuid)"
:class="focusedIndex === index && 'magic-item-focused'" class="magic-item"> :class="focusedIndex === index && 'magic-item-focused'" class="magic-item">
<span class="badge badge-primary badge-sm">Server</span> <span class="magic-badge">Server</span>
<span x-text="server.name"></span> <span x-text="server.name"></span>
</div> </div>
</template> </template>
@ -65,7 +61,7 @@
<template x-for="(destination,index) in filteredDestinations" :key="destination.name ?? destination"> <template x-for="(destination,index) in filteredDestinations" :key="destination.name ?? destination">
<div x-on:click="await set('project',destination.uuid)" <div x-on:click="await set('project',destination.uuid)"
:class="focusedIndex === index && 'magic-item-focused'" class="magic-item"> :class="focusedIndex === index && 'magic-item-focused'" class="magic-item">
<span class=" badge badge-primary badge-sm">Destination</span> <span class=" magic-badge">Destination</span>
<span x-text="destination.name"></span> <span x-text="destination.name"></span>
</div> </div>
</template> </template>
@ -88,7 +84,7 @@
<template x-for="(project,index) in filteredProjects" :key="project.name ?? project"> <template x-for="(project,index) in filteredProjects" :key="project.name ?? project">
<div x-on:click="await set('environment',project.uuid)" <div x-on:click="await set('environment',project.uuid)"
:class="focusedIndex === index + 1 && 'magic-item-focused'" class="magic-item"> :class="focusedIndex === index + 1 && 'magic-item-focused'" class="magic-item">
<span class="badge badge-primary badge-sm">Project</span> <span class="magic-badge">Project</span>
<span x-text="project.name"></span> <span x-text="project.name"></span>
</div> </div>
</template> </template>
@ -98,7 +94,8 @@
{{-- Environments --}} {{-- Environments --}}
<template x-cloak x-if="environmentMenu"> <template x-cloak x-if="environmentMenu">
<div x-on:click.outside="closeMenus"> <div x-on:click.outside="closeMenus">
<input class="magic-input" x-ref="search" x-model="search" placeholder="Select a environment..." <input class="magic-input" x-ref="search" x-model="search"
placeholder="Enter the new environment name or select one..."
x-on:keydown.down="focusNext(environments.length + 1)" x-on:keydown.down="focusNext(environments.length + 1)"
x-on:keydown.up="focusPrev(environments.length + 1)" x-on:keyup.escape="closeMenus" x-on:keydown.up="focusPrev(environments.length + 1)" x-on:keyup.escape="closeMenus"
x-on:keyup.enter="focusedIndex !== '' && await set('jump',filteredEnvironments()[focusedIndex - 1]?.name)" /> x-on:keyup.enter="focusedIndex !== '' && await set('jump',filteredEnvironments()[focusedIndex - 1]?.name)" />
@ -106,12 +103,12 @@
<div x-on:click="await newEnvironment" :class="focusedIndex === 0 && 'magic-item-focused'" <div x-on:click="await newEnvironment" :class="focusedIndex === 0 && 'magic-item-focused'"
class="magic-item"> class="magic-item">
<span>New Environment</span> <span>New Environment</span>
<span x-text="search"></span> <span class="text-warning" x-text="search"></span>
</div> </div>
<template x-for="(environment,index) in filteredEnvironments" :key="environment.name ?? environment"> <template x-for="(environment,index) in filteredEnvironments" :key="environment.name ?? environment">
<div x-on:click="await set('jump',environment.name)" <div x-on:click="await set('jump',environment.name)"
:class="focusedIndex === index + 1 && 'magic-item-focused'" class="magic-item"> :class="focusedIndex === index + 1 && 'magic-item-focused'" class="magic-item">
<span class="badge badge-primary badge-sm">Env</span> <span class="magic-badge">Env</span>
<span x-text="environment.name"></span> <span x-text="environment.name"></span>
</div> </div>
</template> </template>
@ -121,9 +118,9 @@ class="magic-item">
{{-- Projects --}} {{-- Projects --}}
<template x-cloak x-if="projectsMenu"> <template x-cloak x-if="projectsMenu">
<div x-on:click.outside="closeMenus"> <div x-on:click.outside="closeMenus">
<input x-ref="search" x-model="search" class="magic-input" placeholder="Select a project..." <input x-ref="search" x-model="search" class="magic-input"
x-on:keyup.escape="closeMenus" x-on:keydown.down="focusNext(projects.length)" placeholder="Enter the new project name or select one..." x-on:keyup.escape="closeMenus"
x-on:keydown.up="focusPrev(projects.length)" x-on:keydown.down="focusNext(projects.length)" x-on:keydown.up="focusPrev(projects.length)"
x-on:keyup.enter="focusedIndex !== '' && await set('jumpToProject',filteredProjects()[focusedIndex]?.uuid)" /> x-on:keyup.enter="focusedIndex !== '' && await set('jumpToProject',filteredProjects()[focusedIndex]?.uuid)" />
<div class="magic-items"> <div class="magic-items">
<template x-if="projects.length === 0"> <template x-if="projects.length === 0">
@ -134,7 +131,7 @@ class="magic-item">
<template x-for="(project,index) in filteredProjects" :key="project.name ?? project"> <template x-for="(project,index) in filteredProjects" :key="project.name ?? project">
<div x-on:click="await set('jumpToProject',project.uuid)" <div x-on:click="await set('jumpToProject',project.uuid)"
:class="focusedIndex === index && 'magic-item-focused'" class="magic-item"> :class="focusedIndex === index && 'magic-item-focused'" class="magic-item">
<span class="badge badge-primary badge-sm">Jump</span> <span class="magic-badge">Jump</span>
<span x-text="project.name"></span> <span x-text="project.name"></span>
</div> </div>
</template> </template>
@ -157,7 +154,7 @@ class="magic-item">
<template x-for="(destination,index) in filteredDestinations" :key="destination.name ?? destination"> <template x-for="(destination,index) in filteredDestinations" :key="destination.name ?? destination">
<div x-on:click="await set('jumpToDestination',destination.uuid)" <div x-on:click="await set('jumpToDestination',destination.uuid)"
:class="focusedIndex === index && 'magic-item-focused'" class="magic-item"> :class="focusedIndex === index && 'magic-item-focused'" class="magic-item">
<span class="badge badge-primary badge-sm">Jump</span> <span class="magic-badge">Jump</span>
<span x-text="destination.name"></span> <span x-text="destination.name"></span>
</div> </div>
</template> </template>
@ -180,7 +177,7 @@ class="magic-item">
<template x-for="(privateKey,index) in filteredPrivateKeys" :key="privateKey.name ?? privateKey"> <template x-for="(privateKey,index) in filteredPrivateKeys" :key="privateKey.name ?? privateKey">
<div x-on:click="await set('jumpToPrivateKey',privateKey.uuid)" <div x-on:click="await set('jumpToPrivateKey',privateKey.uuid)"
:class="focusedIndex === index && 'magic-item-focused'" class="magic-item"> :class="focusedIndex === index && 'magic-item-focused'" class="magic-item">
<span class="badge badge-primary badge-sm">Jump</span> <span class="magic-badge">Jump</span>
<span x-text="privateKey.name"></span> <span x-text="privateKey.name"></span>
</div> </div>
</template> </template>
@ -203,7 +200,7 @@ class="magic-item">
<template x-for="(source,index) in filteredSources" :key="source.name ?? source"> <template x-for="(source,index) in filteredSources" :key="source.name ?? source">
<div x-on:click="await set('jumpToSource',source)" <div x-on:click="await set('jumpToSource',source)"
:class="focusedIndex === index && 'magic-item-focused'" class="magic-item"> :class="focusedIndex === index && 'magic-item-focused'" class="magic-item">
<span class="badge badge-primary badge-sm">Jump</span> <span class="magic-badge">Jump</span>
<span x-text="source.name"></span> <span x-text="source.name"></span>
</div> </div>
</template> </template>

View File

@ -1,5 +1,104 @@
@auth @auth
<div class="navbar"> <nav class="main-navbar">
<ul class="gap-2 p-1 pt-2 menu">
<li class="{{ request()->is('/') ? 'text-warning' : '' }}">
<a @if (!request()->is('/')) href="/" @endif>
<svg xmlns="http://www.w3.org/2000/svg" class="icon" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
</a>
</li>
<li class="{{ request()->is('server/*') || request()->is('servers') ? 'text-warning' : '' }}">
<a @if (!request()->is('server/*')) href="/servers" @endif>
<svg xmlns="http://www.w3.org/2000/svg" class="icon" 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="M3 4m0 3a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v2a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3z" />
<path d="M3 12m0 3a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v2a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3z" />
<path d="M7 8l0 .01" />
<path d="M7 16l0 .01" />
</svg>
</a>
</li>
<li class="{{ request()->is('project/*') || request()->is('projects') ? 'text-warning' : '' }}">
<a @if (!request()->is('project/*')) href="/projects" @endif>
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 4l-8 4l8 4l8 -4l-8 -4" />
<path d="M4 12l8 4l8 -4" />
<path d="M4 16l8 4l8 -4" />
</svg>
</a>
</li>
@if (auth()->user()->isPartOfRootTeam())
<li class="{{ request()->is('command-center') ? 'text-warning' : '' }}">
<a @if (!request()->is('command-center')) href="/command-center" @endif>
<svg xmlns="http://www.w3.org/2000/svg" class="icon" 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="M5 7l5 5l-5 5" />
<path d="M12 19l7 0" />
</svg>
</a>
</li>
<li
class="{{ request()->is('settings') ? 'absolute bottom-0 pb-4 text-warning' : 'absolute bottom-0 pb-4' }}">
<a @if (!request()->is('settings')) href="/settings" @endif>
<svg xmlns="http://www.w3.org/2000/svg" class="icon" 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="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" />
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
</svg>
</a>
</li>
@endif
</ul>
</nav>
<div class="absolute top-0 right-0 pt-2">
<div class="dropdown dropdown-left">
<label tabindex="0" class="btn btn-ghost no-animation hover:bg-transparent">
<div class="flex items-center justify-center gap-2 avatar placeholder">
<div class="w-8 rounded-full bg-coolgray-300 text-neutral-content">
<span class="text-xs">{{ Str::of(auth()->user()->name)->substr(0, 2)->upper() }}</span>
</div>
<x-chevron-down />
</div>
</label>
<ul tabindex="0" class="p-2 mt-3 shadow menu menu-compact dropdown-content bg-base-100 rounded-box w-52">
<li>
<a href="/profile">
Profile
</a>
</li>
<li>
<a href="/profile/team">
Team
</a>
</li>
@if (auth()->user()->isPartOfRootTeam())
<li>
<livewire:force-upgrade />
</li>
@endif
<form action="/logout" method="POST">
<li>
@csrf
<button>Logout</button>
</li>
</form>
</ul>
</div>
</div>
{{-- <div class="navbar">
<div class="navbar-start"> <div class="navbar-start">
<div class="dropdown"> <div class="dropdown">
<label tabindex="0" class="btn btn-ghost xl:hidden"> <label tabindex="0" class="btn btn-ghost xl:hidden">
@ -21,11 +120,11 @@
</a> </a>
</li> </li>
@endif @endif
{{-- <li> <li>
<a href="/profile"> <a href="/profile">
Profile Profile
</a> </a>
</li> --}} </li>
<li> <li>
<a href="/profile/team"> <a href="/profile/team">
Team Team
@ -49,7 +148,7 @@
</li> </li>
</ul> </ul>
</div> </div>
<div href="/" class="text-xl text-white normal-case btn btn-ghost hover:bg-transparent">Coolify</div> <div class="px-2 text-xl font-bold text-white normal-case">Coolify</div>
<div class="form-control"> <div class="form-control">
<x-magic-bar /> <x-magic-bar />
</div> </div>
@ -68,11 +167,11 @@
</a> </a>
</li> </li>
@endif @endif
{{-- <li> <li>
<a href="/profile"> <a href="/profile">
Profile Profile
</a> </a>
</li> --}} </li>
<li> <li>
<a href="/profile/team"> <a href="/profile/team">
Team Team
@ -96,5 +195,5 @@
</li> </li>
</ul> </ul>
</div> </div>
</div> </div> --}}
@endauth @endauth

View File

@ -1,55 +1,6 @@
<x-layout> <x-layout>
@if ($servers->count() === 0) <h1>Dashboard</h1>
<div class="flex flex-col items-center justify-center h-full pt-32"> <div class="container w-full pt-10 mx-auto">
<div class="">Without a server, you won't be able to do much...</div> Something useful will be here
<div>Let's create <a class="underline text-warning" href="{{ route('server.new') }}">your </div>
first</a> one!</div>
</div>
@else
<h1>Projects </h1>
<div class="flex gap-2">
@forelse ($projects as $project)
<a href="{{ route('project.environments', [$project->uuid]) }}"
class="box">{{ data_get($project, 'name') }}</a>
@empty
<p>No projects found.</p>
@endforelse
</div>
<h1>Servers </h1>
<div class="flex gap-2">
@forelse ($servers as $server)
<a href="{{ route('server.show', [$server->uuid]) }}" class="box">{{ data_get($server, 'name') }}</a>
@empty
<p>No servers found.</p>
@endforelse
</div>
{{-- <h1>Destinations </h1>
<div class="flex gap-2">
@forelse ($destinations as $destination)
<a href="{{ route('destination.show', [$destination->uuid]) }}"
class="box">{{ data_get($destination, 'name') }}</a>
@empty
<p>No destinations found.</p>
@endforelse
</div> --}}
{{-- <h1>Private Keys </h1>
<div class="flex gap-2">
@forelse ($private_keys as $private_key)
<a href="{{ route('private-key.show', [$private_key->uuid]) }}"
class="box">{{ data_get($private_key, 'name') }}</a>
@empty
<p>No servers found.</p>
@endforelse
</div> --}}
{{-- <h1>GitHub Apps </h1>
<div class="flex">
@forelse ($github_apps as $github_app)
<a href="{{ route('source.github.show', [$github_app->uuid]) }}"
class="box">{{ data_get($github_app, 'name') }}</a>
@empty
<p>No servers found.</p>
@endforelse
</div> --}}
@endif
</x-layout> </x-layout>

View File

@ -1,53 +1,57 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title')</title> <head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Styles --> <title>@yield('title')</title>
<style>
html, body {
background-color: #fff;
color: #636b6f;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-weight: 100;
height: 100vh;
margin: 0;
}
.full-height { <!-- Styles -->
height: 100vh; <style>
} html,
body {
background-color: #fff;
color: #636b6f;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-weight: 100;
height: 100vh;
margin: 0;
}
.flex-center { .full-height {
align-items: center; height: 100vh;
display: flex; }
justify-content: center;
}
.position-ref { .flex-center {
position: relative; align-items: center;
} display: flex;
justify-content: center;
}
.content { .position-ref {
text-align: center; position: relative;
} }
.title { .content {
font-size: 36px; text-align: center;
padding: 20px; }
}
</style> .title {
</head> font-size: 36px;
<body> padding: 20px;
<div class="flex-center position-ref full-height"> }
<div class="content"> </style>
<div class="title"> </head>
@yield('message')
</div> <body>
<div class="flex-center position-ref full-height">
<div class="content">
<div class="title">
@yield('message')
</div> </div>
</div> </div>
</body> </div>
</body>
</html> </html>

View File

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

View File

@ -1,34 +1,40 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@if ($application->status === 'running') @if ($application->status === 'running')
<div class="btn-group"> <div class="dropdown dropdown-bottom">
<x-inputs.button isWarning wire:click='stop'>Stop</x-inputs.button> <button tabindex="0"
<div class="bg-transparent border-none dropdown dropdown-hover btn btn-xs no-animation"> class="flex items-center justify-center h-full text-white normal-case rounded bg-primary btn btn-xs hover:bg-primary no-animation">
<button tabindex="0" class="flex items-center justify-center h-full"> Actions
<x-chevron-down /> <x-chevron-down />
</button> </button>
<ul tabindex="0" <ul tabindex="0"
class="text-xs text-white normal-case rounded min-w-max dropdown-content menu bg-coolgray-200"> class="text-xs text-white normal-case rounded min-w-max dropdown-content menu bg-coolgray-200">
<li> <li>
<div wire:click='forceRebuild'>Force deploy without cache</div> <div wire:click='stop'>Stop</div>
</li> </li>
</ul> <li>
</div> <div wire:click='forceRebuild'>Force deploy without cache</div>
</li>
</ul>
</div> </div>
running
@else @else
<div class="btn-group"> <div class="dropdown dropdown-bottom">
<x-inputs.button isHighlighted wire:click='start'>Deploy</x-inputs.button> <button tabindex="0"
<div class="border-none dropdown dropdown-hover btn btn-xs bg-coollabs hover:bg-coollabs-100 no-animation"> class="flex items-center justify-center h-full text-white normal-case rounded bg-primary btn btn-xs hover:bg-primary no-animation">
<button tabindex="0" class="flex items-center justify-center h-full"> Actions
<x-chevron-down /> <x-chevron-down />
</button> </button>
<ul tabindex="0" <ul tabindex="0"
class="text-xs text-white normal-case rounded min-w-max dropdown-content menu bg-coolgray-200"> class="text-xs text-white normal-case rounded min-w-max dropdown-content menu bg-coolgray-200">
<li> <li>
<div wire:click='forceRebuild'>Deploy without cache</div> <div wire:click='start'>Deploy</div>
</li> </li>
</ul> <li>
</div> <div wire:click='forceRebuild'>Deploy without cache</div>
</li>
</ul>
</div> </div>
stopped
@endif @endif
<span wire:poll.5000ms='pollingStatus'> <span wire:poll.5000ms='pollingStatus'>
{{-- @if ($application->status === 'running') {{-- @if ($application->status === 'running')

View File

@ -1,4 +1,4 @@
<div <div
class="flex flex-col-reverse w-full overflow-y-auto border border-solid rounded border-coolgray-300 max-h-[32rem] p-4"> class="flex flex-col-reverse w-full overflow-y-auto border border-solid rounded border-coolgray-300 max-h-[32rem] p-4 text-xs text-white">
<pre @if ($isKeepAliveOn) wire:poll.750ms="polling" @endif>{{ \App\Actions\CoolifyTask\RunRemoteProcess::decodeOutput($activity) }}</pre> <pre class="font-mono whitespace-pre-wrap" @if ($isKeepAliveOn) wire:poll.1000ms="polling" @endif>{{ \App\Actions\CoolifyTask\RunRemoteProcess::decodeOutput($activity) }}</pre>
</div> </div>

View File

@ -2,7 +2,7 @@
<h1>Command Center</h1> <h1>Command Center</h1>
<form class="flex items-end justify-center gap-2" wire:submit.prevent='runCommand'> <form class="flex items-end justify-center gap-2" wire:submit.prevent='runCommand'>
<x-inputs.input placeholder="ls -l" autofocus noDirty noLabel id="command" label="Command" required /> <x-inputs.input placeholder="ls -l" autofocus noDirty noLabel id="command" label="Command" required />
<select wire:model.defer="server"> <x-inputs.select label="Server" id="server" required>
@foreach ($servers as $server) @foreach ($servers as $server)
@if ($loop->first) @if ($loop->first)
<option selected value="{{ $server->uuid }}">{{ $server->name }}</option> <option selected value="{{ $server->uuid }}">{{ $server->name }}</option>
@ -10,7 +10,7 @@
<option value="{{ $server->uuid }}">{{ $server->name }}</option> <option value="{{ $server->uuid }}">{{ $server->name }}</option>
@endif @endif
@endforeach @endforeach
</select> </x-inputs.select>
<x-inputs.button class="btn-xl" type="submit">Run</x-inputs.button> <x-inputs.button class="btn-xl" type="submit">Run</x-inputs.button>
</form> </form>
<div class="container w-full pt-10 mx-auto"> <div class="container w-full pt-10 mx-auto">

View File

@ -1,11 +1,17 @@
<div> <div>
<form class="flex flex-col gap-1" wire:submit.prevent='submit'> <form class="flex flex-col gap-1" wire:submit.prevent='submit'>
<h1>New Server</h1> <div class="flex items-center gap-2">
<h1>New Server</h1>
<x-inputs.button type="submit">
Save
</x-inputs.button>
</div>
<x-inputs.input id="name" label="Name" required /> <x-inputs.input id="name" label="Name" required />
<x-inputs.input id="description" label="Description" /> <x-inputs.input id="description" label="Description" />
<x-inputs.input id="ip" label="IP Address" required /> <x-inputs.input id="ip" label="IP Address" required
<x-inputs.input id="user" label="User" /> helper="Could be IP Address (127.0.0.1) or Domain Name (duckduckgo.com)." />
<x-inputs.input type="number" id="port" label="Port" /> <x-inputs.input id="user" label="User" required />
<x-inputs.input type="number" id="port" label="Port" required />
<label>Private Key</label> <label>Private Key</label>
<x-inputs.select wire:model.defer="private_key_id"> <x-inputs.select wire:model.defer="private_key_id">
<option disabled>Select a private key</option> <option disabled>Select a private key</option>
@ -17,10 +23,7 @@
@endif @endif
@endforeach @endforeach
</x-inputs.select> </x-inputs.select>
<x-inputs.input instantSave noDirty type="checkbox" id="is_part_of_swarm" <x-inputs.checkbox instantSave noDirty id="is_part_of_swarm" label="Is it part of a Swarm cluster?" />
label="Is it part of a Swarm cluster?" />
<x-inputs.button type="submit">
Save
</x-inputs.button>
</form> </form>
</div> </div>

View File

@ -1,6 +1,6 @@
<div> <div>
<form wire:submit.prevent='submit' class="flex flex-col"> <form wire:submit.prevent='submit' class="flex flex-col">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2 border-b-2 border-solid border-coolgray-200">
<h1>Settings</h1> <h1>Settings</h1>
<x-inputs.button type="submit"> <x-inputs.button type="submit">
Save Save

View File

@ -1,10 +0,0 @@
<x-layout>
<h1>Environments</h1>
@foreach ($project->environments as $environment)
<div>
<a href="{{ route('project.resources', [$project->uuid, $environment->name]) }}">
{{ $environment->name }}
</a>
</div>
@endforeach
</x-layout>

View File

@ -0,0 +1,10 @@
<x-layout>
<h1>Environments</h1>
<div class="flex flex-col gap-2">
@foreach ($project->environments as $environment)
<a class="box" href="{{ route('project.resources', [$project->uuid, $environment->name]) }}">
{{ $environment->name }}
</a>
@endforeach
</div>
</x-layout>

View File

@ -0,0 +1,9 @@
<x-layout>
<h1>Projects</h1>
@forelse ($projects as $project)
<a href="{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}"
class="box">{{ $project->name }}</a>
@empty
No project found.
@endforelse
</x-layout>

View File

@ -1,5 +1,6 @@
<x-layout> <x-layout>
<h1>Server</h1> <h1 class="border-b-2 border-solid border-coolgray-200">Server <span
class="text-xs text-neutral-400">{{ $server->name }}</span></h1>
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex pt-6"> <div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex pt-6">
<div class="flex flex-col gap-4 min-w-fit"> <div class="flex flex-col gap-4 min-w-fit">
<a :class="activeTab === 'general' && 'text-white'" <a :class="activeTab === 'general' && 'text-white'"

View File

@ -0,0 +1,13 @@
<x-layout>
<h1>Servers</h1>
@forelse ($servers as $server)
<a href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}"
class="box">{{ $server->name }}</a>
@empty
<div class="flex flex-col items-center justify-center h-full pt-32">
<div class="">Without a server, you won't be able to do much...</div>
<div>Let's create <a class="underline text-warning" href="{{ route('server.new') }}">your
first</a> one!</div>
</div>
@endforelse
</x-layout>

View File

@ -189,6 +189,9 @@
})->name('source.github.show'); })->name('source.github.show');
}); });
Route::middleware(['auth'])->group(function () { Route::middleware(['auth'])->group(function () {
Route::get('/servers', fn () => view('servers', [
'servers' => Server::validated(),
]))->name('servers');
Route::get('/server/new', fn () => view('server.new', [ Route::get('/server/new', fn () => view('server.new', [
'private_keys' => PrivateKey::where('team_id', session('currentTeam')->id)->get(), 'private_keys' => PrivateKey::where('team_id', session('currentTeam')->id)->get(),
]))->name('server.new'); ]))->name('server.new');
@ -235,10 +238,15 @@
}); });
Route::middleware(['auth'])->group(function () { Route::middleware(['auth'])->group(function () {
Route::get(
'/projects',
[ProjectController::class, 'all']
)->name('projects');
Route::get( Route::get(
'/project/{project_uuid}', '/project/{project_uuid}',
[ProjectController::class, 'environments'] [ProjectController::class, 'show']
)->name('project.environments'); )->name('project.show');
Route::get( Route::get(
'/project/{project_uuid}/{environment_name}/new', '/project/{project_uuid}/{environment_name}/new',