feat: tags

ui: improvements
This commit is contained in:
Andras Bacsai 2024-02-02 11:50:28 +01:00
parent 6312c0ba84
commit e7fdff0f69
24 changed files with 589 additions and 346 deletions

View File

@ -45,6 +45,7 @@ public function handle(Service $service)
foreach ($service->databases()->get() as $database) {
$database->forceDelete();
}
$service->tags()->detach();
}
}
}

View File

@ -0,0 +1,154 @@
<?php
namespace App\Http\Controllers\Api;
use App\Actions\Database\StartMariadb;
use App\Actions\Database\StartMongodb;
use App\Actions\Database\StartMysql;
use App\Actions\Database\StartPostgresql;
use App\Actions\Database\StartRedis;
use App\Actions\Service\StartService;
use App\Http\Controllers\Controller;
use App\Models\Tag;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Visus\Cuid2\Cuid2;
class Deploy extends Controller
{
public function deploy(Request $request)
{
$token = auth()->user()->currentAccessToken();
$teamId = data_get($token, 'team_id');
$uuids = $request->query->get('uuid');
$tags = $request->query->get('tag');
$force = $request->query->get('force') ?? false;
if ($uuids && $tags) {
return response()->json(['error' => 'You can only use uuid or tag, not both.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400);
}
if (is_null($teamId)) {
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400);
}
if ($tags) {
return $this->by_tags($tags, $teamId, $force);
} else if ($uuids) {
return $this->by_uuids($uuids, $teamId, $force);
}
return response()->json(['error' => 'You must provide uuid or tag.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400);
}
private function by_uuids(string $uuid, int $teamId, bool $force = false)
{
$uuids = explode(',', $uuid);
$uuids = collect(array_filter($uuids));
if (count($uuids) === 0) {
return response()->json(['error' => 'No UUIDs provided.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400);
}
$message = collect([]);
foreach ($uuids as $uuid) {
$resource = getResourceByUuid($uuid, $teamId);
if ($resource) {
$return_message = $this->deploy_resource($resource, $force);
$message = $message->merge($return_message);
}
}
if ($message->count() > 0) {
return response()->json(['message' => $message->toArray()], 200);
}
return response()->json(['error' => "No resources found.", 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 404);
}
public function by_tags(string $tags, int $team_id, bool $force = false)
{
$tags = explode(',', $tags);
$tags = collect(array_filter($tags));
if (count($tags) === 0) {
return response()->json(['error' => 'No TAGs provided.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400);
}
$message = collect([]);
foreach ($tags as $tag) {
$found_tag = Tag::where(['name' => $tag, 'team_id' => $team_id])->first();
if (!$found_tag) {
$message->push("Tag {$tag} not found.");
continue;
}
$resources = $found_tag->resources()->get();
if ($resources->count() === 0) {
$message->push("No resources found for tag {$tag}.");
continue;
}
foreach ($resources as $resource) {
$return_message = $this->deploy_resource($resource, $force);
$message = $message->merge($return_message);
}
}
if ($message->count() > 0) {
return response()->json(['message' => $message->toArray()], 200);
}
return response()->json(['error' => "No resources found.", 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 404);
}
public function deploy_resource($resource, bool $force = false): Collection
{
$message = collect([]);
$type = $resource->getMorphClass();
if ($type === 'App\Models\Application') {
queue_application_deployment(
application: $resource,
deployment_uuid: new Cuid2(7),
force_rebuild: $force,
);
$message->push("Application {$resource->name} deployment queued.");
} else if ($type === 'App\Models\StandalonePostgresql') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartPostgresql::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
} else if ($type === 'App\Models\StandaloneRedis') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartRedis::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
} else if ($type === 'App\Models\StandaloneMongodb') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartMongodb::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
} else if ($type === 'App\Models\StandaloneMysql') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartMysql::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
} else if ($type === 'App\Models\StandaloneMariadb') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartMariadb::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
} else if ($type === 'App\Models\Service') {
StartService::run($resource);
$message->push("Service {$resource->name} started. It could take a while, be patient.");
}
return $message;
}
}

View File

@ -29,7 +29,7 @@ public function mount()
}
$this->project = $project;
$this->environment = $environment;
$this->applications = $environment->applications->load(['tags'])->sortBy('name');
$this->applications = $environment->applications->load(['tags']);
$this->applications = $this->applications->map(function ($application) {
if (data_get($application, 'environment.project.uuid')) {
$application->hrefLink = route('project.application.configuration', [
@ -40,7 +40,8 @@ public function mount()
}
return $application;
});
$this->postgresqls = $environment->postgresqls->sortBy('name');
ray($this->applications);
$this->postgresqls = $environment->postgresqls->load(['tags'])->sortBy('name');
$this->postgresqls = $this->postgresqls->map(function ($postgresql) {
if (data_get($postgresql, 'environment.project.uuid')) {
$postgresql->hrefLink = route('project.database.configuration', [
@ -51,7 +52,7 @@ public function mount()
}
return $postgresql;
});
$this->redis = $environment->redis->sortBy('name');
$this->redis = $environment->redis->load(['tags'])->sortBy('name');
$this->redis = $this->redis->map(function ($redis) {
if (data_get($redis, 'environment.project.uuid')) {
$redis->hrefLink = route('project.database.configuration', [
@ -62,7 +63,7 @@ public function mount()
}
return $redis;
});
$this->mongodbs = $environment->mongodbs->sortBy('name');
$this->mongodbs = $environment->mongodbs->load(['tags'])->sortBy('name');
$this->mongodbs = $this->mongodbs->map(function ($mongodb) {
if (data_get($mongodb, 'environment.project.uuid')) {
$mongodb->hrefLink = route('project.database.configuration', [
@ -73,7 +74,7 @@ public function mount()
}
return $mongodb;
});
$this->mysqls = $environment->mysqls->sortBy('name');
$this->mysqls = $environment->mysqls->load(['tags'])->sortBy('name');
$this->mysqls = $this->mysqls->map(function ($mysql) {
if (data_get($mysql, 'environment.project.uuid')) {
$mysql->hrefLink = route('project.database.configuration', [
@ -84,7 +85,7 @@ public function mount()
}
return $mysql;
});
$this->mariadbs = $environment->mariadbs->sortBy('name');
$this->mariadbs = $environment->mariadbs->load(['tags'])->sortBy('name');
$this->mariadbs = $this->mariadbs->map(function ($mariadb) {
if (data_get($mariadb, 'environment.project.uuid')) {
$mariadb->hrefLink = route('project.database.configuration', [
@ -95,7 +96,7 @@ public function mount()
}
return $mariadb;
});
$this->services = $environment->services->sortBy('name');
$this->services = $environment->services->load(['tags'])->sortBy('name');
$this->services = $this->services->map(function ($service) {
if (data_get($service, 'environment.project.uuid')) {
$service->hrefLink = route('project.service.configuration', [

View File

@ -9,11 +9,33 @@ class Tags extends Component
{
public $resource = null;
public ?string $new_tag = null;
public $tags = [];
protected $listeners = [
'refresh' => '$refresh',
];
protected $rules = [
'resource.tags.*.name' => 'required|string|min:2',
'new_tag' => 'required|string|min:2'
];
protected $validationAttributes = [
'new_tag' => 'tag'
];
public function mount()
{
$this->tags = Tag::ownedByCurrentTeam()->get();
}
public function addTag(string $id, string $name)
{
try {
if ($this->resource->tags()->where('id', $id)->exists()) {
$this->dispatch('error', 'Duplicate tags.', "Tag <span class='text-warning'>$name</span> already added.");
return;
}
$this->resource->tags()->syncWithoutDetaching($id);
$this->refresh();
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function deleteTag($id, $name)
{
@ -41,6 +63,10 @@ public function submit()
]);
$tags = str($this->new_tag)->trim()->explode(' ');
foreach ($tags as $tag) {
if ($this->resource->tags()->where('name', $tag)->exists()) {
$this->dispatch('error', 'Duplicate tags.', "Tag <span class='text-warning'>$tag</span> already added.");
continue;
}
$found = Tag::where(['name' => $tag, 'team_id' => currentTeam()->id])->first();
if (!$found) {
$found = Tag::create([

View File

@ -2,6 +2,8 @@
namespace App\Livewire\Tags;
use App\Http\Controllers\Api\Deploy;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Tag;
use Livewire\Component;
@ -10,6 +12,38 @@ class Show extends Component
public Tag $tag;
public $resources;
public $webhook = null;
public $deployments_per_tag_per_server = [];
public function get_deployments()
{
try {
$resource_ids = $this->resources->pluck('id');
$this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->whereIn('application_id', $resource_ids)->get([
"id",
"application_id",
"application_name",
"deployment_url",
"pull_request_id",
"server_name",
"server_id",
"status"
])->sortBy('id')->groupBy('server_name')->toArray();
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function redeploy_all()
{
try {
$this->resources->each(function ($resource) {
$deploy = new Deploy();
$deploy->deploy_resource($resource);
});
$this->dispatch('success', 'Mass deployment started.');
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function mount()
{
$tag = Tag::ownedByCurrentTeam()->where('name', request()->tag_name)->first();
@ -19,6 +53,7 @@ public function mount()
$this->webhook = generatTagDeployWebhook($tag->name);
$this->resources = $tag->resources()->get();
$this->tag = $tag;
$this->get_deployments();
}
public function render()
{

View File

@ -49,6 +49,7 @@ protected static function booted()
$application->persistentStorages()->delete();
$application->environment_variables()->delete();
$application->environment_variables_preview()->delete();
$application->tags()->detach();
});
}

View File

@ -20,6 +20,10 @@ public function team()
{
return data_get($this, 'environment.project.team');
}
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function extraFields()
{
$fields = collect([]);

View File

@ -40,8 +40,14 @@ protected static function booted()
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
}
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function team()
{
return data_get($this, 'environment.project.team');

View File

@ -43,8 +43,14 @@ protected static function booted()
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
}
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function team()
{
return data_get($this, 'environment.project.team');

View File

@ -40,8 +40,14 @@ protected static function booted()
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
}
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function team()
{
return data_get($this, 'environment.project.team');

View File

@ -40,8 +40,14 @@ protected static function booted()
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
}
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function link()
{
if (data_get($this, 'environment.project.uuid')) {

View File

@ -35,8 +35,14 @@ protected static function booted()
}
$database->persistentStorages()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
}
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function team()
{
return data_get($this, 'environment.project.team');

View File

@ -110,7 +110,7 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
}
if ($error instanceof UniqueConstraintViolationException) {
if (isset($livewire)) {
return $livewire->dispatch('error', "Duplicate entry found.","Please use a different name.");
return $livewire->dispatch('error', "Duplicate entry found.", "Please use a different name.");
}
return "Duplicate entry found. Please use a different name.";
}
@ -485,8 +485,8 @@ function generatTagDeployWebhook($tag_name)
{
$baseUrl = base_url();
$api = Url::fromString($baseUrl) . '/api/v1';
$endpoint = "/deploy/tag/$tag_name";
$url = $api . $endpoint . "?force=false";
$endpoint = "/deploy?tag=$tag_name";
$url = $api . $endpoint;
return $url;
}
function generateDeployWebhook($resource)

View File

@ -14,6 +14,10 @@ button[isError] {
@apply bg-red-600 hover:bg-red-700;
}
button[isHighlighted] {
@apply bg-coollabs hover:bg-coollabs-100;
}
.scrollbar {
@apply scrollbar-thumb-coollabs-100 scrollbar-track-coolgray-200 scrollbar-w-2;
}

View File

@ -46,9 +46,16 @@ class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5
type="button">Cancel
</x-forms.button>
<div class="flex-1"></div>
<x-forms.button @click="modalOpen=false" class="w-24" isError type="button"
wire:click.prevent='{{ $action }}'>Continue
</x-forms.button>
@if ($isErrorButton)
<x-forms.button @click="modalOpen=false" class="w-24" isError type="button"
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
</x-forms.button>
@endif
</div>
</div>
</div>

View File

@ -102,41 +102,40 @@
</div>
<div class="flex items-center gap-2">
<h3 class="py-4">Deployments </h3>
<h3 class="py-4">Deployments</h3>
@if (count($deployments_per_server) > 0)
<x-loading />
@endif
</div>
{{-- <div wire:poll.4000ms="get_deployments" class="grid grid-cols-1 gap-2 lg:grid-cols-3"> --}}
<div class="grid grid-cols-1">
<div wire:poll.1000ms="get_deployments" class="grid grid-cols-1">
@forelse ($deployments_per_server as $server_name => $deployments)
<h4 class="py-4">{{ $server_name }}</h4>
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
@foreach ($deployments as $deployment)
<a href="{{ data_get($deployment, 'deployment_url') }}" @class([
'gap-2 cursor-pointer box group border-l-2 border-dotted',
'border-white' => data_get($deployment, 'status') === 'queued',
'border-yellow-500' => data_get($deployment, 'status') === 'in_progress',
])>
<div class="flex flex-col mx-6">
<div class="font-bold text-white">
{{ data_get($deployment, 'application_name') }}
</div>
@if (data_get($deployment, 'pull_request_id') !== 0)
<div class="description">
PR #{{ data_get($deployment, 'pull_request_id') }}
<h4 class="py-4">{{ $server_name }}</h4>
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
@foreach ($deployments as $deployment)
<a href="{{ data_get($deployment, 'deployment_url') }}" @class([
'gap-2 cursor-pointer box group border-l-2 border-dotted',
'border-coolgray-500' => data_get($deployment, 'status') === 'queued',
'border-yellow-500' => data_get($deployment, 'status') === 'in_progress',
])>
<div class="flex flex-col mx-6">
<div class="font-bold text-white">
{{ data_get($deployment, 'application_name') }}
</div>
@if (data_get($deployment, 'pull_request_id') !== 0)
<div class="description">
PR #{{ data_get($deployment, 'pull_request_id') }}
</div>
@endif
<div class="description">
{{ str(data_get($deployment, 'status'))->headline() }}
</div>
@endif
<div class="description">
{{ str(data_get($deployment, 'status'))->headline() }}
</div>
</div>
<div class="flex-1"></div>
</a>
@endforeach
<div class="flex-1"></div>
</a>
@endforeach
</div>
@empty
<div>No queued / in progress deployments</div>
<div>No deployments running.</div>
@endforelse
</div>
<script>

View File

@ -5,13 +5,14 @@
</div>
<div class="flex items-end gap-2">
<x-forms.input required id="newProjectName" label="New Project Name" />
<x-forms.button type="submit">Clone</x-forms.button>
<x-forms.button isHighlighted type="submit">Clone</x-forms.button>
</div>
<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 border border-coolgray-500">
<h3>{{ $server->name }}</h3>
<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">
@ -28,7 +29,8 @@
</div>
<h3 class="pt-4 pb-2">Resources</h3>
<div class="grid grid-cols-1 gap-2 p-4 border border-coolgray-500">
<div>These will be cloned to the new project</div>
<div class="grid grid-cols-1 gap-2 p-4 ">
@foreach ($environment->applications->sortBy('name') as $application)
<div>
<div class="flex flex-col">

View File

@ -48,6 +48,9 @@
@click.prevent="activeTab = 'resource-operations'; window.location.hash = 'resource-operations'"
href="#">Resource Operations
</a>
<a :class="activeTab === 'tags' && 'text-white'"
@click.prevent="activeTab = 'tags'; window.location.hash = 'tags'" href="#">Tags
</a>
<a :class="activeTab === 'danger' && 'text-white'"
@click.prevent="activeTab = 'danger';
window.location.hash = 'danger'"
@ -89,6 +92,9 @@
<div x-cloak x-show="activeTab === 'resource-operations'">
<livewire:project.shared.resource-operations :resource="$database" />
</div>
<div x-cloak x-show="activeTab === 'tags'">
<livewire:project.shared.tags :resource="$database" />
</div>
<div x-cloak x-show="activeTab === 'danger'">
<livewire:project.shared.danger :resource="$database" />
</div>

View File

@ -76,106 +76,167 @@ class="items-center justify-center box">+ Add New Resource</a>
</span>
</template>
<template x-for="item in filteredPostgresqls" :key="item.id">
<a class="relative box group" :href="item.hrefLink">
<div class="flex flex-col mx-6">
<div class="font-bold text-white" x-text="item.name"></div>
<div class="description" x-text="item.description"></div>
<span class="relative">
<a class="h-24 box group" :href="item.hrefLink">
<div class="flex flex-col mx-6">
<div class="font-bold text-white" x-text="item.name"></div>
<div class="description" x-text="item.description"></div>
</div>
<template x-if="item.status.startsWith('running')">
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('restarting')">
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
</template>
</a>
<div class="flex gap-1 pt-1 group-hover:text-white group min-h-6">
<template x-for="tag in item.tags">
<div class="px-2 py-1 cursor-pointer description bg-coolgray-100 hover:bg-coolgray-300"
@click.prevent="gotoTag(tag.name)" x-text="tag.name"></div>
</template>
<div class="flex items-center px-2 text-xs cursor-pointer text-neutral-500/20 group-hover:text-white hover:bg-coolgray-300"
@click.prevent="goto(item)">Add tag</div>
</div>
<template x-if="item.status.startsWith('running')">
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('restarting')">
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
</template>
</a>
</span>
</template>
<template x-for="item in filteredRedis" :key="item.id">
<a class="relative box group" :href="item.hrefLink">
<div class="flex flex-col mx-6">
<div class="font-bold text-white" x-text="item.name"></div>
<div class="description" x-text="item.description"></div>
<span class="relative">
<a class="h-24 box group" :href="item.hrefLink">
<div class="flex flex-col mx-6">
<div class="font-bold text-white" x-text="item.name"></div>
<div class="description" x-text="item.description"></div>
</div>
<template x-if="item.status.startsWith('running')">
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('restarting')">
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
</template>
</a>
<div class="flex gap-1 pt-1 group-hover:text-white group min-h-6">
<template x-for="tag in item.tags">
<div class="px-2 py-1 cursor-pointer description bg-coolgray-100 hover:bg-coolgray-300"
@click.prevent="gotoTag(tag.name)" x-text="tag.name"></div>
</template>
<div class="flex items-center px-2 text-xs cursor-pointer text-neutral-500/20 group-hover:text-white hover:bg-coolgray-300"
@click.prevent="goto(item)">Add tag</div>
</div>
<template x-if="item.status.startsWith('running')">
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('restarting')">
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
</template>
</a>
</span>
</template>
<template x-for="item in filteredMongodbs" :key="item.id">
<a class="relative box group" :href="item.hrefLink">
<div class="flex flex-col mx-6">
<div class="font-bold text-white" x-text="item.name"></div>
<div class="description" x-text="item.description"></div>
<span class="relative">
<a class="h-24 box group" :href="item.hrefLink">
<div class="flex flex-col mx-6">
<div class="font-bold text-white" x-text="item.name"></div>
<div class="description" x-text="item.description"></div>
</div>
<template x-if="item.status.startsWith('running')">
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('restarting')">
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
</template>
</a>
<div class="flex gap-1 pt-1 group-hover:text-white group min-h-6">
<template x-for="tag in item.tags">
<div class="px-2 py-1 cursor-pointer description bg-coolgray-100 hover:bg-coolgray-300"
@click.prevent="gotoTag(tag.name)" x-text="tag.name"></div>
</template>
<div class="flex items-center px-2 text-xs cursor-pointer text-neutral-500/20 group-hover:text-white hover:bg-coolgray-300"
@click.prevent="goto(item)">Add tag</div>
</div>
<template x-if="item.status.startsWith('running')">
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('restarting')">
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
</template>
</a>
</span>
</template>
<template x-for="item in filteredMysqls" :key="item.id">
<a class="relative box group" :href="item.hrefLink">
<div class="flex flex-col mx-6">
<div class="font-bold text-white" x-text="item.name"></div>
<div class="description" x-text="item.description"></div>
<span class="relative">
<a class="h-24 box group" :href="item.hrefLink">
<div class="flex flex-col mx-6">
<div class="font-bold text-white" x-text="item.name"></div>
<div class="description" x-text="item.description"></div>
</div>
<template x-if="item.status.startsWith('running')">
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('restarting')">
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
</template>
</a>
<div class="flex gap-1 pt-1 group-hover:text-white group min-h-6">
<template x-for="tag in item.tags">
<div class="px-2 py-1 cursor-pointer description bg-coolgray-100 hover:bg-coolgray-300"
@click.prevent="gotoTag(tag.name)" x-text="tag.name"></div>
</template>
<div class="flex items-center px-2 text-xs cursor-pointer text-neutral-500/20 group-hover:text-white hover:bg-coolgray-300"
@click.prevent="goto(item)">Add tag</div>
</div>
<template x-if="item.status.startsWith('running')">
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('restarting')">
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
</template>
</a>
</span>
</template>
<template x-for="item in filteredMariadbs" :key="item.id">
<a class="relative box group" :href="item.hrefLink">
<div class="flex flex-col mx-6">
<div class="font-bold text-white" x-text="item.name"></div>
<div class="description" x-text="item.description"></div>
<span class="relative">
<a class="h-24 box group" :href="item.hrefLink">
<div class="flex flex-col mx-6">
<div class="font-bold text-white" x-text="item.name"></div>
<div class="description" x-text="item.description"></div>
</div>
<template x-if="item.status.startsWith('running')">
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('restarting')">
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
</template>
</a>
<div class="flex gap-1 pt-1 group-hover:text-white group min-h-6">
<template x-for="tag in item.tags">
<div class="px-2 py-1 cursor-pointer description bg-coolgray-100 hover:bg-coolgray-300"
@click.prevent="gotoTag(tag.name)" x-text="tag.name"></div>
</template>
<div class="flex items-center px-2 text-xs cursor-pointer text-neutral-500/20 group-hover:text-white hover:bg-coolgray-300"
@click.prevent="goto(item)">Add tag</div>
</div>
<template x-if="item.status.startsWith('running')">
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('restarting')">
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
</template>
</a>
</span>
</template>
<template x-for="item in filteredServices" :key="item.id">
<a class="relative box group" :href="item.hrefLink">
<div class="flex flex-col mx-6">
<div class="font-bold text-white" x-text="item.name"></div>
<div class="description" x-text="item.description"></div>
<span class="relative">
<a class="h-24 box group" :href="item.hrefLink">
<div class="flex flex-col mx-6">
<div class="font-bold text-white" x-text="item.name"></div>
<div class="description" x-text="item.description"></div>
</div>
<template x-if="item.status.startsWith('running')">
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('degraded')">
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
</template>
</a>
<div class="flex gap-1 pt-1 group-hover:text-white group min-h-6">
<template x-for="tag in item.tags">
<div class="px-2 py-1 cursor-pointer description bg-coolgray-100 hover:bg-coolgray-300"
@click.prevent="gotoTag(tag.name)" x-text="tag.name"></div>
</template>
<div class="flex items-center px-2 text-xs cursor-pointer text-neutral-500/20 group-hover:text-white hover:bg-coolgray-300"
@click.prevent="goto(item)">Add tag</div>
</div>
<template x-if="item.status.startsWith('running')">
<div class="absolute bg-success -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div class="absolute bg-error -top-1 -left-1 badge badge-xs"></div>
</template>
<template x-if="item.status.startsWith('degraded')">
<div class="absolute bg-warning -top-1 -left-1 badge badge-xs"></div>
</template>
</a>
</span>
</template>
</div>
</div>
@ -184,6 +245,10 @@ class="items-center justify-center box">+ Add New Resource</a>
</div>
<script>
function sortFn(a, b) {
return a.name.localeCompare(b.name)
}
function searchComponent() {
return {
search: '',
@ -203,74 +268,81 @@ function searchComponent() {
},
get filteredApplications() {
if (this.search === '') {
return this.applications;
return Object.values(this.applications).sort(sortFn);
}
this.applications = Object.values(this.applications);
return this.applications.filter(item => {
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
item.fqdn?.toLowerCase().includes(this.search.toLowerCase()) ||
item.description?.toLowerCase().includes(this.search.toLowerCase());
});
item.description?.toLowerCase().includes(this.search.toLowerCase()) ||
item.tags?.some(tag => tag.name.toLowerCase().includes(this.search.toLowerCase()));
}).sort(sortFn);
},
get filteredPostgresqls() {
if (this.search === '') {
return this.postgresqls;
return Object.values(this.postgresqls).sort(sortFn);
}
this.postgresqls = Object.values(this.postgresqls);
return this.postgresqls.filter(item => {
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
item.description?.toLowerCase().includes(this.search.toLowerCase());
});
item.description?.toLowerCase().includes(this.search.toLowerCase()) ||
item.tags?.some(tag => tag.name.toLowerCase().includes(this.search.toLowerCase()));
}).sort(sortFn);
},
get filteredRedis() {
if (this.search === '') {
return this.redis;
return Object.values(this.redis).sort(sortFn);
}
this.redis = Object.values(this.redis);
return this.redis.filter(item => {
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
item.description?.toLowerCase().includes(this.search.toLowerCase());
});
item.description?.toLowerCase().includes(this.search.toLowerCase()) ||
item.tags?.some(tag => tag.name.toLowerCase().includes(this.search.toLowerCase()));
}).sort(sortFn);
},
get filteredMongodbs() {
if (this.search === '') {
return this.mongodbs;
return Object.values(this.mongodbs).sort(sortFn);
}
this.mongodbs = Object.values(this.mongodbs);
return this.mongodbs.filter(item => {
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
item.description?.toLowerCase().includes(this.search.toLowerCase());
});
item.description?.toLowerCase().includes(this.search.toLowerCase()) ||
item.tags?.some(tag => tag.name.toLowerCase().includes(this.search.toLowerCase()));
}).sort(sortFn);
},
get filteredMysqls() {
if (this.search === '') {
return this.mysqls;
return Object.values(this.mysqls).sort(sortFn);
}
this.mysqls = Object.values(this.mysqls);
return this.mysqls.filter(item => {
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
item.description?.toLowerCase().includes(this.search.toLowerCase());
});
item.description?.toLowerCase().includes(this.search.toLowerCase()) ||
item.tags?.some(tag => tag.name.toLowerCase().includes(this.search.toLowerCase()));
}).sort(sortFn);
},
get filteredMariadbs() {
if (this.search === '') {
return this.mariadbs;
return Object.values(this.mariadbs).sort(sortFn);
}
this.mariadbs = Object.values(this.mariadbs);
return this.mariadbs.filter(item => {
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
item.description?.toLowerCase().includes(this.search.toLowerCase());
});
item.description?.toLowerCase().includes(this.search.toLowerCase()) ||
item.tags?.some(tag => tag.name.toLowerCase().includes(this.search.toLowerCase()));
}).sort(sortFn);
},
get filteredServices() {
if (this.search === '') {
return this.services;
return Object.values(this.services).sort(sortFn);
}
this.services = Object.values(this.services);
return this.services.filter(item => {
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
item.description?.toLowerCase().includes(this.search.toLowerCase());
});
item.description?.toLowerCase().includes(this.search.toLowerCase()) ||
item.tags?.some(tag => tag.name.toLowerCase().includes(this.search.toLowerCase()));
}).sort(sortFn);
},
};

View File

@ -30,6 +30,9 @@
@click.prevent="activeTab = 'resource-operations'; window.location.hash = 'resource-operations'"
href="#">Resource Operations
</a>
<a :class="activeTab === 'tags' && 'text-white'"
@click.prevent="activeTab = 'tags'; window.location.hash = 'tags'" href="#">Tags
</a>
<a :class="activeTab === 'danger' && 'text-white'"
@click.prevent="activeTab = 'danger';
window.location.hash = 'danger'"
@ -164,6 +167,9 @@
<div x-cloak x-show="activeTab === 'resource-operations'">
<livewire:project.shared.resource-operations :resource="$service" />
</div>
<div x-cloak x-show="activeTab === 'tags'">
<livewire:project.shared.tags :resource="$service" />
</div>
<div x-cloak x-show="activeTab === 'danger'">
<livewire:project.shared.danger :resource="$service" />
</div>

View File

@ -1,13 +1,32 @@
<div>
<h2>Tags</h2>
@foreach ($this->resource->tags as $tag)
<div>
<div>{{ $tag->name }}</div>
<x-forms.button isError wire:click="deleteTag('{{ $tag->id }}','{{ $tag->name }}')">Delete</x-forms.button>
<div class="flex gap-2 pt-4">
@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 }}')"
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>
</svg>
</div>
@empty
<div>No tags yet</div>
@endforelse
</div>
<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" />
</div>
@endforeach
<form wire:submit='submit'>
<x-forms.input label="Add/Assign a tag" wire:model="new_tag" wire:confirm="Are you sure you want to delete this post?" />
<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>
</div>

View File

@ -1,10 +1,11 @@
<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
<p>No tags yet</p>
<div>No tags yet defined yet. Go to a resource and add a tag there.</div>
@endforelse
</div>
</div>

View File

@ -1,18 +1,58 @@
<div>
<h1>Tag: {{ $tag->name }}</h1>
<div class="">Tag details</div>
<div class="lg:w-[500px] pt-4">
<x-forms.input readonly label="Tag Deploy Webhook URL" id="webhook" />
<div class="flex items-start gap-2">
<div>
<h1>Tag: {{ $tag->name }}</h1>
<div class="pt-2">Tag details</div>
</div>
</div>
<div class="pt-4">
<div class="flex items-end gap-2">
<h3>Resources</h3>
<x-forms.button>Redeploy All</x-forms.button>
<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">
All resources will be redeployed.
</x-new-modal>
</div>
<div class="grid gap-2 pt-4 lg:grid-cols-2">
<div class="grid gap-2 pt-4 lg:grid-cols-4">
@foreach ($resources as $resource)
<div class="box">{{ data_get($resource, 'name') }}</div>
<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>
</a>
@endforeach
</div>
<div class="flex items-center gap-2">
<h3 class="py-4">Deployments</h3>
@if (count($deployments_per_tag_per_server) > 0)
<x-loading />
@endif
</div>
<div wire:poll.1000ms="get_deployments" class="grid grid-cols-1">
@forelse ($deployments_per_tag_per_server as $server_name => $deployments)
<h4 class="py-4">{{ $server_name }}</h4>
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
@foreach ($deployments as $deployment)
<a href="{{ data_get($deployment, 'deployment_url') }}" @class([
'gap-2 cursor-pointer box group border-l-2 border-dotted',
'border-coolgray-500' => data_get($deployment, 'status') === 'queued',
'border-yellow-500' => data_get($deployment, 'status') === 'in_progress',
])>
<div class="flex flex-col mx-6">
<div class="font-bold text-white">
{{ data_get($deployment, 'application_name') }}
</div>
<div class="description">
{{ str(data_get($deployment, 'status'))->headline() }}
</div>
</div>
<div class="flex-1"></div>
</a>
@endforeach
</div>
@empty
<div>No deployments running.</div>
@endforelse
</div>
</div>
</div>

View File

@ -6,6 +6,7 @@
use App\Actions\Database\StartPostgresql;
use App\Actions\Database\StartRedis;
use App\Actions\Service\StartService;
use App\Http\Controllers\Api\Deploy;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Tag;
use App\Models\User;
@ -33,191 +34,25 @@
Route::get('/health', function () {
return 'OK';
});
Route::group([
'middleware' => $middlewares,
'prefix' => 'v1'
], function () {
Route::get('/deployments', function () {
return ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->get([
"id",
"server_id",
"status"
])->groupBy("server_id")->map(function ($item) {
return $item;
})->toArray();
});
});
// Route::group([
// 'middleware' => $middlewares,
// 'prefix' => 'v1'
// ], function () {
// Route::get('/deployments', function () {
// return ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->get([
// "id",
// "server_id",
// "status"
// ])->groupBy("server_id")->map(function ($item) {
// return $item;
// })->toArray();
// });
// });
Route::group([
'middleware' => ['auth:sanctum'],
'prefix' => 'v1'
], function () {
Route::get('/deploy', function (Request $request) {
$token = auth()->user()->currentAccessToken();
$teamId = data_get($token, 'team_id');
$uuid = $request->query->get('uuid');
$uuids = explode(',', $uuid);
$uuids = collect(array_filter($uuids));
$force = $request->query->get('force') ?? false;
if (is_null($teamId)) {
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400);
}
if (count($uuids) === 0) {
return response()->json(['error' => 'No UUIDs provided.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400);
}
$message = collect([]);
foreach ($uuids as $uuid) {
$resource = getResourceByUuid($uuid, $teamId);
if ($resource) {
$type = $resource->getMorphClass();
if ($type === 'App\Models\Application') {
queue_application_deployment(
application: $resource,
deployment_uuid: new Cuid2(7),
force_rebuild: $force,
);
$message->push("Application {$resource->name} deployment queued.");
} else if ($type === 'App\Models\StandalonePostgresql') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartPostgresql::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
} else if ($type === 'App\Models\StandaloneRedis') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartRedis::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
} else if ($type === 'App\Models\StandaloneMongodb') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartMongodb::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
} else if ($type === 'App\Models\StandaloneMysql') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartMysql::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
} else if ($type === 'App\Models\StandaloneMariadb') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartMariadb::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
} else if ($type === 'App\Models\Service') {
StartService::run($resource);
$message->push("Service {$resource->name} started. It could take a while, be patient.");
}
}
}
if ($message->count() > 0) {
return response()->json(['message' => $message->toArray()], 200);
}
return response()->json(['error' => "No resources found.", 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 404);
});
Route::get('/deploy/tag/{tag_name}', function (Request $request) {
$token = auth()->user()->currentAccessToken();
$team_id = data_get($token, 'team_id');
$tag_name = $request->route('tag_name');
$force = $request->query->get('force') ?? false;
if (is_null($team_id)) {
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400);
}
$message = collect([]);
$tag = Tag::where(['name' => $tag_name, 'team_id' => $team_id])->first();
if (!$tag) {
return response()->json(['error' => 'Tag not found.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 404);
}
$resources = $tag->resources()->get();
if ($resources->count() === 0) {
return response()->json(['error' => 'No resources found.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 404);
}
foreach ($resources as $resource) {
if ($resource) {
$type = $resource->getMorphClass();
if ($type === 'App\Models\Application') {
queue_application_deployment(
application: $resource,
deployment_uuid: new Cuid2(7),
force_rebuild: $force,
);
$message->push("Application {$resource->name} deployment queued.");
} else if ($type === 'App\Models\StandalonePostgresql') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartPostgresql::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
} else if ($type === 'App\Models\StandaloneRedis') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartRedis::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
} else if ($type === 'App\Models\StandaloneMongodb') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartMongodb::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
} else if ($type === 'App\Models\StandaloneMysql') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartMysql::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
} else if ($type === 'App\Models\StandaloneMariadb') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartMariadb::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
} else if ($type === 'App\Models\Service') {
StartService::run($resource);
$message->push("Service {$resource->name} started. It could take a while, be patient.");
}
}
}
ray($resources);
if ($message->count() > 0) {
return response()->json(['message' => $message->toArray()], 200);
}
return response()->json(['error' => "No resources found.", 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 404);
});
Route::get('/deploy', [Deploy::class, 'deploy']);
});
Route::middleware(['throttle:5'])->group(function () {