feat: tags
ui: improvements
This commit is contained in:
parent
6312c0ba84
commit
e7fdff0f69
@ -45,6 +45,7 @@ public function handle(Service $service)
|
||||
foreach ($service->databases()->get() as $database) {
|
||||
$database->forceDelete();
|
||||
}
|
||||
$service->tags()->detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
154
app/Http/Controllers/Api/Deploy.php
Normal file
154
app/Http/Controllers/Api/Deploy.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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', [
|
||||
|
@ -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([
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -49,6 +49,7 @@ protected static function booted()
|
||||
$application->persistentStorages()->delete();
|
||||
$application->environment_variables()->delete();
|
||||
$application->environment_variables_preview()->delete();
|
||||
$application->tags()->detach();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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([]);
|
||||
|
@ -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');
|
||||
|
@ -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');
|
||||
|
@ -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');
|
||||
|
@ -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')) {
|
||||
|
@ -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');
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
},
|
||||
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
197
routes/api.php
197
routes/api.php
@ -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 () {
|
||||
|
Loading…
Reference in New Issue
Block a user