diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php index f7464a697..1a411cf59 100644 --- a/app/Actions/Service/DeleteService.php +++ b/app/Actions/Service/DeleteService.php @@ -45,6 +45,7 @@ class DeleteService foreach ($service->databases()->get() as $database) { $database->forceDelete(); } + $service->tags()->detach(); } } } diff --git a/app/Http/Controllers/Api/Deploy.php b/app/Http/Controllers/Api/Deploy.php new file mode 100644 index 000000000..322d73e67 --- /dev/null +++ b/app/Http/Controllers/Api/Deploy.php @@ -0,0 +1,154 @@ +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; + } +} diff --git a/app/Livewire/Project/Resource/Index.php b/app/Livewire/Project/Resource/Index.php index b296f855c..5f1753c9e 100644 --- a/app/Livewire/Project/Resource/Index.php +++ b/app/Livewire/Project/Resource/Index.php @@ -29,7 +29,7 @@ class Index extends Component } $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 @@ class Index extends Component } 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 @@ class Index extends Component } 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 @@ class Index extends Component } 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 @@ class Index extends Component } 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 @@ class Index extends Component } 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 @@ class Index extends Component } 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', [ diff --git a/app/Livewire/Project/Shared/Tags.php b/app/Livewire/Project/Shared/Tags.php index 32b9679ae..db330b15c 100644 --- a/app/Livewire/Project/Shared/Tags.php +++ b/app/Livewire/Project/Shared/Tags.php @@ -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 $name 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 @@ class Tags extends Component ]); $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 $tag already added."); + continue; + } $found = Tag::where(['name' => $tag, 'team_id' => currentTeam()->id])->first(); if (!$found) { $found = Tag::create([ diff --git a/app/Livewire/Tags/Show.php b/app/Livewire/Tags/Show.php index 0363445c8..6a61a0851 100644 --- a/app/Livewire/Tags/Show.php +++ b/app/Livewire/Tags/Show.php @@ -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 @@ class Show extends Component $this->webhook = generatTagDeployWebhook($tag->name); $this->resources = $tag->resources()->get(); $this->tag = $tag; + $this->get_deployments(); } public function render() { diff --git a/app/Models/Application.php b/app/Models/Application.php index e1528d56b..b91acdcff 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -49,6 +49,7 @@ class Application extends BaseModel $application->persistentStorages()->delete(); $application->environment_variables()->delete(); $application->environment_variables_preview()->delete(); + $application->tags()->detach(); }); } diff --git a/app/Models/Service.php b/app/Models/Service.php index adbf6c500..b6d5e86d3 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -20,6 +20,10 @@ class Service extends BaseModel { return data_get($this, 'environment.project.team'); } + public function tags() + { + return $this->morphToMany(Tag::class, 'taggable'); + } public function extraFields() { $fields = collect([]); diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index 8b3f3412d..4d56f11a3 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -40,8 +40,14 @@ class StandaloneMariadb extends BaseModel $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'); diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index de2e7f2eb..939af0974 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -43,8 +43,14 @@ class StandaloneMongodb extends BaseModel $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'); diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index c9608f1cc..8bcc0d9fe 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -40,8 +40,14 @@ class StandaloneMysql extends BaseModel $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'); diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index 577904260..fb6ad944d 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -40,8 +40,14 @@ class StandalonePostgresql extends BaseModel $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')) { diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index 5f4279183..73fa61a6c 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -35,8 +35,14 @@ class StandaloneRedis extends BaseModel } $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'); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 812c23598..ad64b2550 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -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) diff --git a/resources/css/app.css b/resources/css/app.css index 924369e98..dbde2a2ad 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -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; } diff --git a/resources/views/components/new-modal.blade.php b/resources/views/components/new-modal.blade.php index 6c6dcc3e3..d04a40226 100644 --- a/resources/views/components/new-modal.blade.php +++ b/resources/views/components/new-modal.blade.php @@ -46,9 +46,16 @@ type="button">Cancel
- Continue - + @if ($isErrorButton) + Continue + + @else + Continue + + @endif + diff --git a/resources/views/livewire/dashboard.blade.php b/resources/views/livewire/dashboard.blade.php index 04d4918b4..143efc339 100644 --- a/resources/views/livewire/dashboard.blade.php +++ b/resources/views/livewire/dashboard.blade.php @@ -102,41 +102,40 @@
-

Deployments

+

Deployments

@if (count($deployments_per_server) > 0) @endif
-{{--
--}} -
+
@forelse ($deployments_per_server as $server_name => $deployments) -

{{ $server_name }}

-
- @foreach ($deployments as $deployment) - data_get($deployment, 'status') === 'queued', - 'border-yellow-500' => data_get($deployment, 'status') === 'in_progress', - ])> -
-
- {{ data_get($deployment, 'application_name') }} -
- @if (data_get($deployment, 'pull_request_id') !== 0) -