Compare commits

...

20 Commits

Author SHA1 Message Date
Andras Bacsai
818399bc23 Merge pull request #1700 from coollabsio/next
v4.0.0-beta.205
2024-02-02 12:41:46 +01:00
Andras Bacsai
e7fdff0f69 feat: tags
ui: improvements
2024-02-02 11:50:28 +01:00
Andras Bacsai
6312c0ba84 feat: tags and tag deploy webhooks 2024-02-01 15:38:12 +01:00
Andras Bacsai
44efe0b5e1 Update versions and fix code formatting 2024-02-01 11:59:20 +01:00
Andras Bacsai
de7d584648 Merge pull request #1694 from coollabsio/next
v4.0.0-beta.204
2024-02-01 10:54:27 +01:00
Andras Bacsai
b9f12d2586 fix: duplicate domain check 2024-02-01 10:53:05 +01:00
Andras Bacsai
c76e8bb0de fix: migrate to new modal 2024-01-31 16:14:12 +01:00
Andras Bacsai
3b655f8e3f Update filebrowser image tag to use filebrowser/filebrowser:s6 2024-01-31 15:16:06 +01:00
Andras Bacsai
2b9df41444 fix: create dynamic directory 2024-01-31 15:04:08 +01:00
Andras Bacsai
628fec6904 fix: sentry error 2024-01-31 14:22:48 +01:00
Andras Bacsai
f36135cbfc fix: sentry 2024-01-31 14:20:57 +01:00
Andras Bacsai
75fe005055 fix: sentry error 2024-01-31 14:19:45 +01:00
Andras Bacsai
8ff7aeb78b ui: new modal component 2024-01-31 14:18:59 +01:00
Andras Bacsai
f1a9e28d5a fix: sentry 2024-01-31 14:18:51 +01:00
Andras Bacsai
843cd90ee5 Update exception type in generate_github_installation_token function 2024-01-31 13:47:16 +01:00
Andras Bacsai
1cbfd03912 fix: sentry fix 2024-01-31 13:46:40 +01:00
Andras Bacsai
ce60a39dc5 Throw RuntimeException instead of Exception when no resource is found in ScheduledTaskJob 2024-01-31 13:45:58 +01:00
Andras Bacsai
f1e4395a83 Refactor shared variable type validation 2024-01-31 13:43:23 +01:00
Andras Bacsai
52fd7ad571 fix: not able to use other shared envs 2024-01-31 13:40:15 +01:00
Andras Bacsai
5f797ec0ae Update version and release numbers 2024-01-31 10:28:18 +01:00
75 changed files with 1086 additions and 595 deletions

View File

@@ -27,7 +27,7 @@ class StartProxy
$server->save();
if ($server->isSwarm()) {
$commands = $commands->merge([
"mkdir -p $proxy_path && cd $proxy_path",
"mkdir -p $proxy_path/dynamic && cd $proxy_path",
"echo 'Creating required Docker Compose file.'",
"echo 'Starting coolify-proxy.'",
"cd $proxy_path && docker stack deploy -c docker-compose.yml coolify-proxy",
@@ -35,7 +35,7 @@ class StartProxy
]);
} else {
$commands = $commands->merge([
"mkdir -p $proxy_path && cd $proxy_path",
"mkdir -p $proxy_path/dynamic && cd $proxy_path",
"echo 'Creating required Docker Compose file.'",
"echo 'Pulling docker image.'",
'docker compose pull',

View File

@@ -45,6 +45,7 @@ class DeleteService
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

@@ -488,7 +488,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} else {
$this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}.");
}
ray('asddf');
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->clone_repository();

View File

@@ -39,7 +39,7 @@ class ScheduledTaskJob implements ShouldQueue
} else if ($application = $task->application()->first()) {
$this->resource = $application;
} else {
throw new \Exception('ScheduledTaskJob failed: No resource found.');
throw new \RuntimeException('ScheduledTaskJob failed: No resource found.');
}
$this->team = Team::find($task->team_id);
}

View File

@@ -9,6 +9,7 @@ class DeleteEnvironment extends Component
{
public array $parameters;
public int $environment_id;
public bool $disabled = false;
public function mount()
{

View File

@@ -9,6 +9,7 @@ class DeleteProject extends Component
{
public array $parameters;
public int $project_id;
public bool $disabled = false;
public function mount()
{

View File

@@ -29,7 +29,7 @@ class Index extends Component
}
$this->project = $project;
$this->environment = $environment;
$this->applications = $environment->applications->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,8 +40,9 @@ class Index extends Component
}
return $application;
});
$this->postgresqls = $environment->postgresqls->sortBy('name');
$this->postgresqls = $this->postgresqls->map(function ($postgresql) {
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', [
'project_uuid' => data_get($postgresql, 'environment.project.uuid'),
@@ -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', [

View File

@@ -8,7 +8,7 @@ use Livewire\Component;
class Configuration extends Component
{
public Service $service;
public ?Service $service = null;
public $applications;
public $databases;
public array $parameters;

View File

@@ -17,8 +17,8 @@ class Danger extends Component
{
$this->modalId = new Cuid2(7);
$parameters = get_route_parameters();
$this->projectUuid = $parameters['project_uuid'];
$this->environmentName = $parameters['environment_name'];
$this->projectUuid = data_get($parameters, 'project_uuid');
$this->environmentName = data_get($parameters, 'environment_name');
}
public function delete()

View File

@@ -32,6 +32,13 @@ class Add extends Component
public function submit()
{
$this->validate();
if (str($this->value)->startsWith('{{') && str($this->value)->endsWith('}}')) {
$type = str($this->value)->after("{{")->before(".")->value;
if (!collect(SHARED_VARIABLE_TYPES)->contains($type)) {
$this->dispatch('error', 'Invalid shared variable type.', "Valid types are: team, project, environment.");
return;
}
}
$this->dispatch('saveKey', [
'key' => $this->key,
'value' => $this->value,

View File

@@ -71,12 +71,26 @@ class All extends Component
continue;
}
$found->value = $variable;
if (str($found->value)->startsWith('{{') && str($found->value)->endsWith('}}')) {
$type = str($found->value)->after("{{")->before(".")->value;
if (!collect(SHARED_VARIABLE_TYPES)->contains($type)) {
$this->dispatch('error', 'Invalid shared variable type.', "Valid types are: team, project, environment.");
return;
}
}
$found->save();
continue;
} else {
$environment = new EnvironmentVariable();
$environment->key = $key;
$environment->value = $variable;
if (str($environment->value)->startsWith('{{') && str($environment->value)->endsWith('}}')) {
$type = str($environment->value)->after("{{")->before(".")->value;
if (!collect(SHARED_VARIABLE_TYPES)->contains($type)) {
$this->dispatch('error', 'Invalid shared variable type.', "Valid types are: team, project, environment.");
return;
}
}
$environment->is_build_time = false;
$environment->is_preview = $isPreview ? true : false;
switch ($this->resource->type()) {

View File

@@ -50,7 +50,8 @@ class Show extends Component
$this->isLocked = true;
}
}
public function serialize() {
public function serialize()
{
data_forget($this->env, 'real_value');
if ($this->env->getMorphClass() === 'App\Models\SharedEnvironmentVariable') {
data_forget($this->env, 'is_build_time');
@@ -80,11 +81,18 @@ class Show extends Component
} else {
$this->validate();
}
if (str($this->env->value)->startsWith('{{') && str($this->env->value)->endsWith('}}')) {
$type = str($this->env->value)->after("{{")->before(".")->value;
if (!collect(SHARED_VARIABLE_TYPES)->contains($type)) {
$this->dispatch('error', 'Invalid shared variable type.', "Valid types are: team, project, environment.");
return;
}
}
$this->serialize();
$this->env->save();
$this->dispatch('success', 'Environment variable updated successfully.');
$this->dispatch('refreshEnvs');
} catch(\Exception $e) {
} catch (\Exception $e) {
return handleError($e);
}
}

View File

@@ -20,8 +20,8 @@ class ResourceOperations extends Component
public function mount()
{
$parameters = get_route_parameters();
$this->projectUuid = $parameters['project_uuid'];
$this->environmentName = $parameters['environment_name'];
$this->projectUuid = data_get($parameters, 'project_uuid');
$this->environmentName = data_get($parameters, 'environment_name');
$this->projects = Project::ownedByCurrentTeam()->get();
$this->servers = currentTeam()->servers;
}

View File

@@ -0,0 +1,88 @@
<?php
namespace App\Livewire\Project\Shared;
use App\Models\Tag;
use Livewire\Component;
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)
{
try {
$found_more_tags = Tag::where(['name' => $name, 'team_id' => currentTeam()->id])->first();
$this->resource->tags()->detach($id);
if ($found_more_tags->resources()->get()->count() == 0) {
$found_more_tags->delete();
}
$this->refresh();
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function refresh()
{
$this->resource->load(['tags']);
$this->new_tag = null;
}
public function submit()
{
try {
$this->validate([
'new_tag' => 'required|string|min:2'
]);
$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([
'name' => $tag,
'team_id' => currentTeam()->id
]);
}
$this->resource->tags()->syncWithoutDetaching($found->id);
}
$this->refresh();
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.project.shared.tags');
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Livewire\Tags;
use App\Models\Tag;
use Livewire\Component;
class Index extends Component
{
public $tags = [];
public function mount() {
$this->tags = Tag::where('team_id', currentTeam()->id)->get()->unique('name')->sortBy('name');
}
public function render()
{
return view('livewire.tags.index');
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Livewire\Tags;
use App\Http\Controllers\Api\Deploy;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Tag;
use Livewire\Component;
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();
if (!$tag) {
return redirect()->route('tags.index');
}
$this->webhook = generatTagDeployWebhook($tag->name);
$this->resources = $tag->resources()->get();
$this->tag = $tag;
$this->get_deployments();
}
public function render()
{
return view('livewire.tags.show');
}
}

View File

@@ -12,7 +12,11 @@ class Invitations extends Component
public function deleteInvitation(int $invitation_id)
{
TeamInvitation::find($invitation_id)->delete();
$initiation_found = TeamInvitation::find($invitation_id);
if (!$initiation_found) {
return $this->dispatch('error', 'Invitation not found.');
}
$initiation_found->delete();
$this->refreshInvitations();
$this->dispatch('success', 'Invitation revoked.');
}

View File

@@ -67,7 +67,8 @@ class Create extends Component
$this->storage->save();
return redirect()->route('team.storage.show', $this->storage->uuid);
} catch (\Throwable $e) {
return handleError($e, $this);
$this->dispatch('error', 'Failed to create storage.', $e->getMessage());
// return handleError($e, $this);
}
}
}

View File

@@ -33,9 +33,9 @@ class Form extends Component
{
try {
$this->storage->testConnection(shouldSave: true);
return $this->dispatch('success', 'Connection is working. Tested with "ListObjectsV2" action.');
return $this->dispatch('success', 'Connection is working.', 'Tested with "ListObjectsV2" action.');
} catch (\Throwable $e) {
return handleError($e, $this);
$this->dispatch('error', 'Failed to create storage.', $e->getMessage());
}
}

View File

@@ -49,6 +49,7 @@ class Application extends BaseModel
$application->persistentStorages()->delete();
$application->environment_variables()->delete();
$application->environment_variables_preview()->delete();
$application->tags()->detach();
});
}
@@ -211,6 +212,10 @@ class Application extends BaseModel
: explode(',', $this->ports_exposes)
);
}
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function team()
{
return data_get($this, 'environment.project.team');

View File

@@ -91,7 +91,7 @@ class EnvironmentVariable extends Model
}
private function get_real_environment_variables(?string $environment_variable = null, $resource = null): string|null
{
if (!$environment_variable) {
if (!$environment_variable || !$resource) {
return null;
}
$environment_variable = trim($environment_variable);
@@ -100,6 +100,9 @@ class EnvironmentVariable extends Model
$variable = Str::after($environment_variable, "{$type}.");
$variable = Str::before($variable, '}}');
$variable = Str::of($variable)->trim()->value;
if (!collect(SHARED_VARIABLE_TYPES)->contains($type)) {
return $variable;
}
if ($type === 'environment') {
$id = $resource->environment->id;
} else if ($type === 'project') {

View File

@@ -64,10 +64,13 @@ class Project extends BaseModel
}
public function mysqls()
{
return $this->hasMany(StandaloneMysql::class, Environment::class);
return $this->hasManyThrough(StandaloneMysql::class, Environment::class);
}
public function mariadbs()
{
return $this->hasMany(StandaloneMariadb::class, Environment::class);
return $this->hasManyThrough(StandaloneMariadb::class, Environment::class);
}
public function resource_count() {
return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count();
}
}

View File

@@ -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([]);

View File

@@ -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');

View File

@@ -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');

View File

@@ -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');

View File

@@ -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')) {

View File

@@ -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');

32
app/Models/Tag.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
class Tag extends BaseModel
{
protected $guarded = [];
public function name(): Attribute
{
return Attribute::make(
get: fn ($value) => strtolower($value),
set: fn ($value) => strtolower($value)
);
}
static public function ownedByCurrentTeam()
{
return Tag::whereTeamId(currentTeam()->id)->orderBy('name');
}
public function applications()
{
return $this->morphedByMany(Application::class, 'taggable');
}
public function resources() {
return $this->applications();
}
}

View File

@@ -34,3 +34,5 @@ const SUPPORTED_OS = [
'centos fedora rhel ol rocky',
'sles opensuse-leap opensuse-tumbleweed'
];
const SHARED_VARIABLE_TYPES = ['team', 'project', 'environment'];

View File

@@ -29,7 +29,7 @@ function generate_github_installation_token(GithubApp $source)
'Accept' => 'application/vnd.github.machine-man-preview+json'
])->post("{$source->api_url}/app/installations/{$source->installation_id}/access_tokens");
if ($token->failed()) {
throw new \Exception("Failed to get access token for " . $source->name . " with error: " . $token->json()['message']);
throw new RuntimeException("Failed to get access token for " . $source->name . " with error: " . $token->json()['message']);
}
return $token->json()['token'];
}

View File

@@ -110,9 +110,9 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
}
if ($error instanceof UniqueConstraintViolationException) {
if (isset($livewire)) {
return $livewire->dispatch('error', "A resource with the same name already exists.");
return $livewire->dispatch('error', "Duplicate entry found.", "Please use a different name.");
}
return "A resource with the same name already exists.";
return "Duplicate entry found. Please use a different name.";
}
if ($error instanceof Throwable) {
@@ -481,7 +481,14 @@ function queryResourcesByUuid(string $uuid)
if ($mariadb) return $mariadb;
return $resource;
}
function generatTagDeployWebhook($tag_name)
{
$baseUrl = base_url();
$api = Url::fromString($baseUrl) . '/api/v1';
$endpoint = "/deploy?tag=$tag_name";
$url = $api . $endpoint;
return $url;
}
function generateDeployWebhook($resource)
{
$baseUrl = base_url();
@@ -937,7 +944,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'service_id' => $resource->id,
])->first();
['command' => $command, 'forService' => $forService, 'generatedValue' => $generatedValue, 'port' => $port] = parseEnvVariable($value);
if ($command->value() === 'FQDN' || $command->value() === 'URL') {
if ($command?->value() === 'FQDN' || $command?->value() === 'URL') {
if (Str::lower($forService) === $serviceName) {
$fqdn = generateFqdn($resource->server, $containerName);
} else {
@@ -1357,7 +1364,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'application_id' => $resource->id,
])->first();
['command' => $command, 'forService' => $forService, 'generatedValue' => $generatedValue, 'port' => $port] = parseEnvVariable($value);
if ($command->value() === 'FQDN' || $command->value() === 'URL') {
if ($command?->value() === 'FQDN' || $command?->value() === 'URL') {
if (Str::lower($forService) === $serviceName) {
$fqdn = generateFqdn($server, $containerName);
} else {
@@ -1670,15 +1677,21 @@ function ip_match($ip, $cidrs, &$match = null)
function check_fqdn_usage(ServiceApplication|Application $own_resource)
{
$domains = collect($own_resource->fqdns)->map(function ($domain) {
return Url::fromString($domain)->getHost();
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
return str($domain)->replace('http://', '')->replace('https://', '');
});
$apps = Application::all();
foreach ($apps as $app) {
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) {
$naked_domain = Url::fromString($domain)->getHost();
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
$naked_domain = str($domain)->replace('http://', '')->replace('https://', '')->value();
if ($domains->contains($naked_domain)) {
if ($app->uuid !== $own_resource->uuid ) {
if ($app->uuid !== $own_resource->uuid) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource.");
}
}
@@ -1688,7 +1701,10 @@ function check_fqdn_usage(ServiceApplication|Application $own_resource)
foreach ($apps as $app) {
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) {
$naked_domain = Url::fromString($domain)->getHost();
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
$naked_domain = str($domain)->replace('http://', '')->replace('https://', '')->value();
if ($domains->contains($naked_domain)) {
if ($app->uuid !== $own_resource->uuid) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource.");

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.203',
'release' => '4.0.0-beta.205',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),

View File

@@ -1,48 +0,0 @@
<?php
declare(strict_types=1);
return [
/**
* Add an additional second for every 100th word of the toast messages.
*
* Supported: true | false
*/
'accessibility' => true,
/**
* The vertical alignment of the toast container.
*
* Supported: "bottom", "middle" or "top"
*/
'alignment' => 'top',
/**
* Allow users to close toast messages prematurely.
*
* Supported: true | false
*/
'closeable' => true,
/**
* The on-screen duration of each toast.
*
* Minimum: 3000 (in milliseconds)
*/
'duration' => 5000,
/**
* The horizontal position of each toast.
*
* Supported: "center", "left" or "right"
*/
'position' => 'center',
/**
* Whether messages passed as translation keys should be translated automatically.
*
* Supported: true | false
*/
'translate' => true,
];

View File

@@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.203';
return '4.0.0-beta.205';

View File

@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('tags', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('name')->unique();
$table->foreignId('team_id')->nullable()->constrained()->onDelete('cascade');
$table->timestamps();
});
Schema::create('taggables', function (Blueprint $table) {
$table->unsignedBigInteger('tag_id');
$table->unsignedBigInteger('taggable_id');
$table->string('taggable_type');
$table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
$table->unique(['tag_id', 'taggable_id', 'taggable_type'], 'taggable_unique'); // Composite unique index
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('taggables');
Schema::dropIfExists('tags');
}
};

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;
}
@@ -76,7 +80,7 @@ a {
}
.box-without-bg {
@apply flex p-2 transition-colors min-h-full hover:text-white hover:no-underline min-h-[4rem];
@apply flex p-2 transition-colors hover:text-white hover:no-underline min-h-[4rem];
}
.description {

View File

@@ -4,7 +4,7 @@
class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a>
<ul class="flex flex-col h-full gap-4 menu flex-nowrap">
<li title="Dashboard">
<a class="hover:bg-transparent" href="/">
<a class="hover:bg-transparent" href="/">
<svg xmlns="http://www.w3.org/2000/svg" class="{{ request()->is('/') ? 'text-warning icon' : 'icon' }}"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@@ -13,7 +13,7 @@
</a>
</li>
<li title="Servers">
<a class="hover:bg-transparent" href="/servers">
<a class="hover:bg-transparent" href="/servers">
<svg xmlns="http://www.w3.org/2000/svg"
class="{{ request()->is('server/*') || request()->is('servers') ? 'text-warning icon' : 'icon' }}"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
@@ -28,7 +28,7 @@
</a>
</li>
<li title="Projects">
<a class="hover:bg-transparent" href="/projects">
<a class="hover:bg-transparent" href="/projects">
<svg xmlns="http://www.w3.org/2000/svg"
class="{{ request()->is('project/*') || request()->is('projects') ? 'text-warning icon' : 'icon' }}"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
@@ -41,7 +41,7 @@
</a>
</li>
<li title="Command Center">
<a class="hover:bg-transparent" href="/command-center">
<a class="hover:bg-transparent" href="/command-center">
<svg xmlns="http://www.w3.org/2000/svg"
class="{{ request()->is('command-center') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
@@ -53,7 +53,7 @@
</a>
</li>
<li title="Source">
<a class="hover:bg-transparent" href="{{ route('source.all') }}">
<a class="hover:bg-transparent" href="{{ route('source.all') }}">
<svg class="icon" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="m6.793 1.207l.353.354l-.353-.354ZM1.207 6.793l-.353-.354l.353.354Zm0 1.414l.354-.353l-.354.353Zm5.586 5.586l-.354.353l.354-.353Zm1.414 0l-.353-.354l.353.354Zm5.586-5.586l.353.354l-.353-.354Zm0-1.414l-.354.353l.354-.353ZM8.207 1.207l.354-.353l-.354.353ZM6.44.854L.854 6.439l.707.707l5.585-5.585L6.44.854ZM.854 8.56l5.585 5.585l.707-.707l-5.585-5.585l-.707.707Zm7.707 5.585l5.585-5.585l-.707-.707l-5.585 5.585l.707.707Zm5.585-7.707L8.561.854l-.707.707l5.585 5.585l.707-.707Zm0 2.122a1.5 1.5 0 0 0 0-2.122l-.707.707a.5.5 0 0 1 0 .708l.707.707ZM6.44 14.146a1.5 1.5 0 0 0 2.122 0l-.707-.707a.5.5 0 0 1-.708 0l-.707.707ZM.854 6.44a1.5 1.5 0 0 0 0 2.122l.707-.707a.5.5 0 0 1 0-.708L.854 6.44Zm6.292-4.878a.5.5 0 0 1 .708 0L8.56.854a1.5 1.5 0 0 0-2.122 0l.707.707Zm-2 1.293l1 1l.708-.708l-1-1l-.708.708ZM7.5 5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 6V5Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 4.5H8ZM7.5 4a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 3v1Zm0-1A1.5 1.5 0 0 0 6 4.5h1a.5.5 0 0 1 .5-.5V3Zm.646 2.854l1.5 1.5l.707-.708l-1.5-1.5l-.707.708ZM10.5 8a.5.5 0 0 1-.5-.5H9A1.5 1.5 0 0 0 10.5 9V8Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 12 7.5h-1Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 10.5 6v1Zm0-1A1.5 1.5 0 0 0 9 7.5h1a.5.5 0 0 1 .5-.5V6ZM7 5.5v4h1v-4H7Zm.5 5.5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 12v-1Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 10.5H8Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 9v1Zm0-1A1.5 1.5 0 0 0 6 10.5h1a.5.5 0 0 1 .5-.5V9Z" />
@@ -61,7 +61,7 @@
</a>
</li>
<li title="Security">
<a class="hover:bg-transparent" href="{{ route('security.private-key.index') }}">
<a class="hover:bg-transparent" href="{{ route('security.private-key.index') }}">
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
@@ -70,7 +70,7 @@
</a>
</li>
<li title="Teams">
<a class="hover:bg-transparent" href="{{ route('team.index') }}">
<a class="hover:bg-transparent" href="{{ route('team.index') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
@@ -83,6 +83,18 @@
</svg>
</a>
</li>
<li title="Tags">
<a class="hover:bg-transparent" href="{{ route('tags.index') }}">
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path
d="M3 8v4.172a2 2 0 0 0 .586 1.414l5.71 5.71a2.41 2.41 0 0 0 3.408 0l3.592-3.592a2.41 2.41 0 0 0 0-3.408l-5.71-5.71A2 2 0 0 0 9.172 6H5a2 2 0 0 0-2 2" />
<path d="m18 19l1.592-1.592a4.82 4.82 0 0 0 0-6.816L15 6m-8 4h-.01" />
</g>
</svg>
</a>
</li>
<div class="flex-1"></div>
@if (isInstanceAdmin() && !isCloud())
@@ -103,7 +115,7 @@
</a>
</li>
<li title="Profile">
<a class="hover:bg-transparent" href="/profile">
<a class="hover:bg-transparent" href="/profile">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
@@ -116,7 +128,7 @@
@if (isInstanceAdmin())
<li title="Settings" class="mt-auto">
<a class="hover:bg-transparent" href="/settings">
<a class="hover:bg-transparent" href="/settings">
<svg xmlns="http://www.w3.org/2000/svg"
class="{{ request()->is('settings*') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"

View File

@@ -0,0 +1,63 @@
@props([
'title' => 'Are you sure?',
'buttonTitle' => 'Open Modal',
'isErrorButton' => false,
'disabled' => false,
'action' => 'delete',
])
<div x-data="{ modalOpen: false }" @keydown.escape.window="modalOpen = false" :class="{ 'z-40': modalOpen }"
class="relative w-auto h-auto">
@if ($disabled)
<x-forms.button isError disabled>{{ $buttonTitle }}</x-forms.button>
@elseif ($isErrorButton)
<x-forms.button isError @click="modalOpen=true">{{ $buttonTitle }}</x-forms.button>
@else
<x-forms.button @click="modalOpen=true">{{ $buttonTitle }}</x-forms.button>
@endif
<template x-teleport="body">
<div x-show="modalOpen" class="fixed top-0 left-0 z-[99] flex items-center justify-center w-screen h-screen"
x-cloak>
<div x-show="modalOpen" x-transition:enter="ease-out duration-100" x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-100"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" @click="modalOpen=false"
class="absolute inset-0 w-full h-full bg-black bg-opacity-20 backdrop-blur-sm"></div>
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
class="relative w-full py-6 border rounded shadow-lg bg-coolgray-100 px-7 border-coolgray-300 sm:max-w-lg">
<div class="flex items-center justify-between pb-3">
<h3 class="text-2xl font-bold">{{ $title }}</h3>
<button @click="modalOpen=false"
class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 text-white rounded-full hover:bg-coolgray-300">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="relative w-auto pb-8">
{{ $slot }}
</div>
<div class="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2">
<x-forms.button @click="modalOpen=false" class="w-24 bg-coolgray-200 hover:bg-coolgray-300"
type="button">Cancel
</x-forms.button>
<div class="flex-1"></div>
@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>
</template>
</div>

View File

@@ -22,20 +22,7 @@
if (typeof options.html != 'undefined') html = options.html;
window.dispatchEvent(new CustomEvent('toast-show', { detail: { type: type, message: message, description: description, position: position, html: html } }));
}
window.customToastHTML = `
<div class='relative flex items-start justify-center p-4'>
<div class='flex flex-col'>
<p class='text-sm font-medium text-gray-800'>New Friend Request</p>
<p class='mt-1 text-xs leading-none text-gray-800'>Friend request from John Doe.</p>
<div class='flex mt-3'>
<button type='button' @click='burnToast(toast.id)' class='inline-flex items-center px-2 py-1 text-xs font-semibold text-white bg-indigo-600 rounded shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600'>Accept</button>
<button type='button' @click='burnToast(toast.id)' class='inline-flex items-center px-2 py-1 ml-3 text-xs font-semibold text-gray-900 bg-white rounded shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50'>Decline</button>
</div>
</div>
</div>
`" class="relative space-y-5">
}" class="relative space-y-5">
<template x-teleport="body">
<ul x-data="{
toasts: [],
@@ -369,10 +356,10 @@ window.customToastHTML = `
}, 5);
}, 4000);"
@mouseover="toastHovered=true" @mouseout="toastHovered=false"
class="absolute w-full duration-200 ease-out select-none sm:max-w-xs"
class="absolute w-full duration-100 ease-out sm:max-w-xs"
:class="{ 'toast-no-description': !toast.description }">
<span
class="relative flex flex-col items-start shadow-[0_5px_15px_-3px_rgb(0_0_0_/_0.08)] w-full transition-all duration-200 ease-out bg-coolgray-200 border border-coolgray-100 sm:rounded-md sm:max-w-xs group"
class="relative flex flex-col items-start shadow-[0_5px_15px_-3px_rgb(0_0_0_/_0.08)] w-full transition-all duration-100 ease-out bg-coolgray-100 border border-coolgray-200 rounded sm:max-w-xs group"
:class="{ 'p-4': !toast.html, 'p-0': toast.html }">
<template x-if="!toast.html">
<div class="relative">
@@ -403,12 +390,12 @@ window.customToastHTML = `
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM11.9996 7C12.5519 7 12.9996 7.44772 12.9996 8V12C12.9996 12.5523 12.5519 13 11.9996 13C11.4474 13 10.9996 12.5523 10.9996 12V8C10.9996 7.44772 11.4474 7 11.9996 7ZM12.001 14.99C11.4488 14.9892 11.0004 15.4363 10.9997 15.9886L10.9996 15.9986C10.9989 16.5509 11.446 16.9992 11.9982 17C12.5505 17.0008 12.9989 16.5537 12.9996 16.0014L12.9996 15.9914C13.0004 15.4391 12.5533 14.9908 12.001 14.99Z"
fill="currentColor"></path>
</svg>
<p class="font-bold leading-2 text-neutral-200"
<p class="leading-2 text-neutral-200"
x-html="toast.message">
</p>
</div>
<p x-show="toast.description" :class="{ 'pl-5': toast.type!='default' }"
class="mt-1.5 text-xs leading-2 opacity-90" x-html="toast.description"></p>
class="mt-1.5 text-xs leading-2 opacity-90 whitespace-pre-wrap" x-html="toast.description"></p>
</div>
</template>
<template x-if="toast.html">

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

@@ -1,9 +1,4 @@
<div>
<x-modal yesOrNo modalId="deleteDestination" modalTitle="Delete Destination">
<x-slot:modalBody>
<p>This destination will be deleted. It is not reversible. <br>Please think again.</p>
</x-slot:modalBody>
</x-modal>
<form class="flex flex-col">
<div class="flex items-center gap-2">
<h1>Destination</h1>
@@ -11,9 +6,9 @@
Save
</x-forms.button>
@if ($destination->network !== 'coolify')
<x-forms.button isError isModal modalId="deleteDestination">
Delete
</x-forms.button>
<x-new-modal isErrorButton buttonTitle="Delete Destination">
This destination will be deleted. It is not reversible. <br>Please think again.
</x-new-modal>
@endif
</div>

View File

@@ -14,7 +14,7 @@
</x-forms.select>
{{-- <x-forms.checkbox type="checkbox" id="is_swarm" label="Is it a Swarm network?" /> --}}
<x-forms.button type="submit">
Save Destination
Continue
</x-forms.button>
</form>
</div>

View File

@@ -63,6 +63,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'" href="#">Danger Zone
</a>
@@ -112,6 +115,9 @@
<div x-cloak x-show="activeTab === 'resource-operations'">
<livewire:project.shared.resource-operations :resource="$application" />
</div>
<div x-cloak x-show="activeTab === 'tags'">
<livewire:project.shared.tags :resource="$application" />
</div>
<div x-cloak x-show="activeTab === 'danger'">
<livewire:project.shared.danger :resource="$application" />
</div>

View File

@@ -112,9 +112,7 @@
helper="If set, it will tag the built image with this tag too. <br><br>Example: If you set it to 'latest', it will push the image with the commit sha tag + with the latest tag."
label="Docker Image Tag" />
@endif
@endif
</div>
@endif

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

@@ -1,8 +1,3 @@
<div>
<x-modal yesOrNo modalId="deleteEnvironment" modalTitle="Delete Environment">
<x-slot:modalBody>
<p>This environment will be deleted. It is not reversible. <br>Please think again.</p>
</x-slot:modalBody>
</x-modal>
<x-forms.button isError isModal modalId="deleteEnvironment"> Delete Environment</x-forms.button>
</div>
<x-new-modal isErrorButton buttonTitle="Delete Environment" disabled="{{ $disabled }}">
This environment will be deleted. It is not reversible. <br>Please think again.
</x-new-modal>

View File

@@ -1,10 +1,3 @@
<div>
<x-modal yesOrNo modalId="deleteProject" modalTitle="Delete Project">
<x-slot:modalBody>
<p>This project will be deleted. It is not reversible. <br>Please think again.</p>
</x-slot:modalBody>
</x-modal>
<x-forms.button isError isModal modalId="deleteProject">
Delete Project
</x-forms.button>
</div>
<x-new-modal isErrorButton buttonTitle="Delete Project" disabled="{{ $disabled }}">
This project will be deleted. It is not reversible. <br>Please think again.
</x-new-modal>

View File

@@ -5,6 +5,7 @@
<div class="flex items-end gap-2">
<h2>General</h2>
<x-forms.button type="submit">Save</x-forms.button>
<livewire:project.delete-project :disabled="$project->resource_count() > 0" :project_id="$project->id" />
</div>
<div class="flex gap-2">
<x-forms.input label="Name" id="project.name" />
@@ -23,7 +24,8 @@
Add</button>
</x-slide-over>
</div>
<div class="flex items-center gap-2 pb-4">You can use these variables anywhere with <span class="text-warning">@{{project.VARIABLENAME}}</span><x-helper
<div class="flex items-center gap-2 pb-4">You can use these variables anywhere with <span
class="text-warning">@{{ project.VARIABLENAME }}</span><x-helper
helper="More info <a class='text-white underline' href='https://coolify.io/docs/environment-variables#shared-variables' target='_blank'>here</a>."></x-helper>
</div>
<div class="flex flex-col gap-2">

View File

@@ -3,6 +3,7 @@
<div class="flex items-end gap-2">
<h1>Environment: {{ data_get($environment, 'name') }}</h1>
<x-forms.button type="submit">Save</x-forms.button>
<livewire:project.delete-environment :disabled="!$environment->isEmpty()" :environment_id="$environment->id" />
</div>
<nav class="flex pt-2 pb-10">
<ol class="flex items-center">

View File

@@ -7,7 +7,6 @@
href="{{ route('project.clone-me', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => request()->route('environment_name')]) }}">
Clone
</a>
<livewire:project.delete-environment :environment_id="$environment->id" />
@else
<a href="{{ route('project.resource.create', ['project_uuid' => request()->route('project_uuid'), 'environment_name' => request()->route('environment_name')]) }} "
class="font-normal text-white normal-case border-none rounded hover:no-underline btn btn-primary btn-sm no-animation">+
@@ -17,6 +16,7 @@
Clone
</a>
@endif
<livewire:project.delete-environment :disabled="!$environment->isEmpty()" :environment_id="$environment->id" />
</div>
<nav class="flex pt-2 pb-10">
<ol class="flex items-center">
@@ -46,126 +46,197 @@
@else
<div x-data="searchComponent()">
<x-forms.input placeholder="Search for name, fqdn..." class="w-full" x-model="search" />
<div class="grid gap-2 pt-4 lg:grid-cols-2">
<div class="grid gap-4 pt-4 lg:grid-cols-4">
<template x-for="item in filteredApplications" :key="item.id">
<a class="relative box group" :href="item.hrefLink">
<div class="flex flex-col mx-6">
<div class="pb-2 font-bold text-white" x-text="item.name"></div>
<div class="description" x-text="item.description"></div>
<div class="description" x-text="item.fqdn"></div>
<span class="relative">
<a class="h-24 box group" :href="item.hrefLink">
<div class="flex flex-col mx-6">
<div class="pb-2 font-bold text-white" x-text="item.name"></div>
<div class="description" x-text="item.description"></div>
<div class="description" x-text="item.fqdn"></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 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>
@@ -174,6 +245,10 @@
</div>
<script>
function sortFn(a, b) {
return a.name.localeCompare(b.name)
}
function searchComponent() {
return {
search: '',
@@ -184,76 +259,90 @@
mysqls: @js($mysqls),
mariadbs: @js($mariadbs),
services: @js($services),
gotoTag(tag) {
window.location.href = '/tags/' + tag;
},
goto(item) {
const hrefLink = item.hrefLink;
window.location.href = `${hrefLink}#tags`;
},
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,16 +1,11 @@
<div>
<x-modal yesOrNo modalId="{{ $modalId }}" modalTitle="Delete Resource">
<x-slot:modalBody>
<p>This resource will be deleted. It is not reversible. <br>Please think again.</p>
</x-slot:modalBody>
</x-modal>
<h2>Danger Zone</h2>
<div class="">Woah. I hope you know what are you doing.</div>
<h4 class="pt-4">Delete Resource</h4>
<div class="pb-4">This will stop your containers, delete all related data, etc. Beware! There is no coming
back!
</div>
<x-forms.button isError isModal modalId="{{ $modalId }}">
Delete
</x-forms.button>
<x-new-modal isErrorButton buttonTitle="Delete">
This resource will be deleted. It is not reversible. <br>Please think again.
</x-new-modal>
</div>

View File

@@ -1,10 +1,4 @@
<div>
<x-modal yesOrNo modalId="{{ $modalId }}" modalTitle="Delete Environment Variable">
<x-slot:modalBody>
<p>Are you sure you want to delete this environment variable <span
class="font-bold text-warning">({{ $env->key }})</span>?</p>
</x-slot:modalBody>
</x-modal>
<form wire:submit='submit'
class="flex flex-col gap-2 p-4 m-2 border lg:items-center border-coolgray-300 lg:m-0 lg:p-0 lg:border-0 lg:flex-row">
@if ($isLocked)
@@ -38,9 +32,10 @@
@endif
<div class="flex gap-2">
@if ($isLocked)
<x-forms.button isError isModal modalId="{{ $modalId }}">
Delete
</x-forms.button>
<x-new-modal isErrorButton buttonTitle="Delete">
You will delete environment variable <span
class="font-bold text-warning">{{ $env->key }}</span>.
</x-new-modal>
@else
@if ($isDisabled)
<x-forms.button disabled type="submit">
@@ -49,9 +44,10 @@
<x-forms.button wire:click='lock'>
Lock
</x-forms.button>
<x-forms.button disabled isError isModal modalId="{{ $modalId }}">
Delete
</x-forms.button>
<x-new-modal isErrorButton buttonTitle="Delete">
You will delete environment variable <span
class="font-bold text-warning">{{ $env->key }}</span>.
</x-new-modal>
@else
<x-forms.button type="submit">
Update
@@ -59,9 +55,10 @@
<x-forms.button wire:click='lock'>
Lock
</x-forms.button>
<x-forms.button isError isModal modalId="{{ $modalId }}">
Delete
</x-forms.button>
<x-new-modal isErrorButton buttonTitle="Delete">
You will delete environment variable <span
class="font-bold text-warning">{{ $env->key }}</span>.
</x-new-modal>
@endif
@endif
</div>

View File

@@ -13,7 +13,7 @@
<x-forms.input label="Only Show Number of Lines" placeholder="1000" required id="numberOfLines"></x-forms.input>
<x-forms.button type="submit">Refresh</x-forms.button>
</form>
<div id="screen" x-data="{ fullscreen: false, alwaysScroll: false, intervalId: null }" :class="fullscreen ? 'fullscreen' : 'container w-full py-4 mx-auto'">
<div id="screen" x-data="{ fullscreen: false, alwaysScroll: false, intervalId: null }" :class="fullscreen ? 'fullscreen' : 'w-full py-4 mx-auto'">
<div class="relative flex flex-col-reverse w-full p-4 pt-6 overflow-y-auto text-white bg-coolgray-100 scrollbar border-coolgray-300"
:class="fullscreen ? '' : 'max-h-[40rem] border border-solid rounded'">
<button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4" x-on:click="makeFullscreen"><svg
@@ -33,8 +33,8 @@
stroke-width="2" d="M12 5v14m4-4l-4 4m-4-4l4 4" />
</svg></button>
<button title="Fullscreen" x-show="!fullscreen" class="absolute top-2 right-8"
x-on:click="makeFullscreen"><svg class="fixed icon" viewBox="0 0 24 24"
<button title="Fullscreen" x-show="!fullscreen" class="absolute top-2 right-2"
x-on:click="makeFullscreen"><svg class=" icon" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<g fill="none">
<path

View File

@@ -1,11 +1,4 @@
<div>
<x-modal yesOrNo modalId="{{ $modalId }}" modalTitle="Delete Scheduled Task">
<x-slot:modalBody>
<p>Are you sure you want to delete this scheduled task <span
class="font-bold text-warning">({{ $task->name }})</span>?</p>
</x-slot:modalBody>
</x-modal>
<h1>Scheduled Task</h1>
@if ($type === 'application')
<livewire:project.application.heading :application="$resource" />
@@ -20,11 +13,9 @@
<x-forms.button type="submit">
Save
</x-forms.button>
<x-forms.button isError isModal modalId="{{ $modalId }}">
Delete
</x-forms.button>
<x-new-modal isErrorButton buttonTitle="Delete Scheduled Task">
You will delete scheduled task <span class="font-bold text-warning">{{ $task->name }}</span>.
</x-new-modal>
</div>
</div>
<div class="flex w-full gap-2">

View File

@@ -1,12 +1,4 @@
<div>
<x-modal yesOrNo modalId="{{ $modalId }}" modalTitle="Delete Storage">
<x-slot:modalBody>
<p>This storage will be deleted <span class="font-bold text-warning">({{ $storage->name }})</span>. It is
not
reversible. <br>Please think again.</p>
</x-slot:modalBody>
</x-modal>
<form wire:submit='submit' class="flex flex-col gap-2 xl:items-end xl:flex-row">
@if ($isReadOnly)
@if ($isFirst)
@@ -32,9 +24,12 @@
<x-forms.button type="submit">
Update
</x-forms.button>
<x-forms.button isError isModal modalId="{{ $modalId }}">
Delete
</x-forms.button>
<x-new-modal isErrorButton buttonTitle="Delete">
This storage will be deleted <span class="font-bold text-warning">{{ $storage->name }}</span>. It
is
not
reversible. <br>Please think again.
</x-new-modal>
</div>
@endif
</form>

View File

@@ -0,0 +1,32 @@
<div>
<h2>Tags</h2>
<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>
<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

@@ -3,9 +3,7 @@
<h1>Environments</h1>
<x-forms.button class="btn" onclick="newEnvironment.showModal()">+ Add</x-forms.button>
<livewire:project.add-environment :project="$project" />
@if ($project->applications->count() === 0)
<livewire:project.delete-project :project_id="$project->id" />
@endif
<livewire:project.delete-project :disabled="$project->resource_count() > 0" :project_id="$project->id" />
</div>
<div class="text-xs truncate subtitle lg:text-sm">{{ $project->name }}</div>
<div class="grid gap-2 lg:grid-cols-2">

View File

@@ -1,21 +1,16 @@
<div>
<x-security.navbar />
<div x-data="{ showPrivateKey: false }">
<x-modal yesOrNo modalId="deletePrivateKey" modalTitle="Delete Private Key">
<x-slot:modalBody>
<p>This private key will be deleted. It is not reversible. <br>Please think again.</p>
</x-slot:modalBody>
</x-modal>
<form class="flex flex-col gap-2" wire:submit='changePrivateKey'>
<div class="flex items-end gap-2">
<h2>Private Key</h2>
<x-forms.button type="submit">
Save
</x-forms.button>
@if ($private_key->id > 0)
<x-forms.button isError isModal modalId="deletePrivateKey">
Delete
</x-forms.button>
@if (data_get($private_key, 'id') > 0)
<x-new-modal isErrorButton buttonTitle="Delete">
This private key will be deleted. It is not reversible. <br>Please think again.
</x-new-modal>
@endif
</div>
<x-forms.input id="private_key.name" label="Name" required />
@@ -36,7 +31,7 @@
Hide
</div>
</div>
@if ($private_key->is_git_related)
@if (data_get($private_key, 'is_git_related'))
<div class="w-48">
<x-forms.checkbox id="private_key.is_git_related" disabled label="Is used by a Git App?" />
</div>

View File

@@ -1,9 +1,4 @@
<div>
<x-modal yesOrNo modalId="deleteServer" modalTitle="Delete Server">
<x-slot:modalBody>
<p>This server will be deleted. It is not reversible. <br>Please think again..</p>
</x-slot:modalBody>
</x-modal>
@if ($server->id !== 0)
<h2 class="pt-4">Danger Zone</h2>
<div class="">Woah. I hope you know what are you doing.</div>
@@ -12,13 +7,13 @@
back!
</div>
@if ($server->definedResources()->count() > 0)
<x-forms.button disabled isError isModal modalId="deleteServer">
Delete
</x-forms.button>
<x-new-modal disabled isErrorButton buttonTitle="Delete">
This server will be deleted. It is not reversible. <br>Please think again.
</x-new-modal>
@else
<x-forms.button isError isModal modalId="deleteServer">
Delete
</x-forms.button>
<x-new-modal isErrorButton buttonTitle="Delete">
This server will be deleted. It is not reversible. <br>Please think again.
</x-new-modal>
@endif
<div class="flex flex-col">
@forelse ($server->definedResources() as $resource)
@@ -26,7 +21,7 @@
<h3 class="pt-4">Resources</h3>
@endif
@if ($resource->link())
<a class="flex gap-2 p-1 hover:bg-coolgray-100 hover:no-underline" href="{{ $resource->link() }}">
<a class="flex gap-2 p-1 hover:bg-coolgray-100 hover:no-underline" href="{{ $resource->link() }}">
<div class="w-64">{{ str($resource->type())->headline() }}</div>
<div>{{ $resource->name }}</div>
</a>
@@ -46,7 +41,7 @@
<h3 class="pt-4">Resources</h3>
@endif
@if ($resource->link())
<a class="flex gap-2 p-1 hover:bg-coolgray-100 hover:no-underline" href="{{ $resource->link() }}">
<a class="flex gap-2 p-1 hover:bg-coolgray-100 hover:no-underline" href="{{ $resource->link() }}">
<div class="w-64">{{ str($resource->type())->headline() }}</div>
<div>{{ $resource->name }}</div>
</a>

View File

@@ -1,15 +1,13 @@
<div>
<x-modal yesOrNo modalId="changeLocalhost" modalTitle="Change Localhost" action="submit">
<x-slot:modalBody>
<p>You could lost a lot of functionalities if you change the server details of the server where Coolify is
running on.<br>Please think again.</p>
</x-slot:modalBody>
</x-modal>
<form wire:submit='submit' class="flex flex-col">
<div class="flex gap-2">
<h2>General</h2>
@if ($server->id === 0)
<x-forms.button isModal modalId="changeLocalhost">Save</x-forms.button>
<x-new-modal buttonTitle="Save" title="Change Localhost" action="submit">
You could lost a lot of functionalities if you change the server details of the server where Coolify
is
running on.<br>Please think again.
</x-new-modal>
@else
<x-forms.button type="submit">Save</x-forms.button>
@endif

View File

@@ -1,9 +1,4 @@
<div>
<x-modal yesOrNo modalId="deleteSource" modalTitle="Delete Source">
<x-slot:modalBody>
<p>This source will be deleted. It is not reversible. <br>Please think again.</p>
</x-slot:modalBody>
</x-modal>
@if (data_get($github_app, 'app_id'))
<form wire:submit='submit'>
<div class="flex items-center gap-2">
@@ -18,9 +13,9 @@
</x-forms.button>
</a>
@endif
<x-forms.button isError isModal modalId="deleteSource">
Delete
</x-forms.button>
<x-new-modal isErrorButton buttonTitle="Delete">
This source will be deleted. It is not reversible. <br>Please think again.
</x-new-modal>
</div>
</div>
<div class="subtitle">Your Private GitHub App for private repositories.</div>
@@ -77,9 +72,9 @@
<div class="flex items-center gap-2 pb-4">
<h1>GitHub App</h1>
<div class="flex gap-2">
<x-forms.button isError isModal modalId="deleteSource">
Delete
</x-forms.button>
<x-new-modal isErrorButton buttonTitle="Delete">
This source will be deleted. It is not reversible. <br>Please think again.
</x-new-modal>
</div>
</div>
<div class="mb-10 rounded alert alert-warning">

View File

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

View File

@@ -0,0 +1,58 @@
<div>
<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 ">
<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-4">
@foreach ($resources as $resource)
<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

@@ -28,11 +28,6 @@
</div>
@endif
<div>
<x-modal yesOrNo modalId="deleteTeam" modalTitle="Delete Team">
<x-slot:modalBody>
<p>This team be deleted. It is not reversible. <br>Please think again.</p>
</x-slot:modalBody>
</x-modal>
<h2>Danger Zone</h2>
<div class="pb-4">Woah. I hope you know what are you doing.</div>
<h4 class="pb-4">Delete Team</h4>
@@ -45,9 +40,9 @@
@else
@if (currentTeam()->isEmpty())
<div class="pb-4">This will delete your team. Beware! There is no coming back!</div>
<x-forms.button isError isModal modalId="deleteTeam">
Delete
</x-forms.button>
<x-new-modal isErrorButton buttonTitle="Delete">
This team be deleted. It is not reversible. <br>Please think again.
</x-new-modal>
@else
<div>
<div class="pb-4">You need to delete the following resources to be able to delete the team:</div>

View File

@@ -1,10 +1,4 @@
<div>
<x-modal yesOrNo modalId="deleteS3Storage" modalTitle="Delete S3 Storage">
<x-slot:modalBody>
<p>This storage will be deleted. It is not reversible. Your data won't be touched!<br>Please think again..
</p>
</x-slot:modalBody>
</x-modal>
<form class="flex flex-col gap-2 pb-6" wire:submit='submit'>
<div class="flex items-start gap-2">
<div class="pb-4">
@@ -22,9 +16,9 @@
<x-forms.button wire:click="test_s3_connection">
Validate Connection
</x-forms.button>
<x-forms.button isError isModal modalId="deleteS3Storage">
Delete
</x-forms.button>
<x-new-modal isErrorButton buttonTitle="Delete">
This storage will be deleted. It is not reversible. Your data won't be touched!<br>Please think again.
</x-new-modal>
</div>
<div class="flex gap-2">
<x-forms.input label="Name" id="storage.name" />

View File

@@ -1,90 +0,0 @@
<div role="status" id="toaster" x-data="toasterHub(@js($toasts), @js($config))" @class([
'fixed z-50 p-4 w-full flex flex-col pointer-events-none sm:p-6',
'bottom-0' => $alignment->is('bottom'),
'top-1/2 -translate-y-1/2' => $alignment->is('middle'),
'top-0' => $alignment->is('top'),
'items-start' => $position->is('left'),
'items-center' => $position->is('center'),
'items-end' => $position->is('right'),
])>
<template x-for="toast in toasts" :key="toast.id">
<div x-show="toast.isVisible" x-init="$nextTick(() => toast.show($el))" @if ($alignment->is('bottom'))
x-transition:enter-start="translate-y-12 opacity-0"
x-transition:enter-end="translate-y-0 opacity-100"
@elseif($alignment->is('top'))
x-transition:enter-start="-translate-y-12 opacity-0"
x-transition:enter-end="translate-y-0 opacity-100"
@else
x-transition:enter-start="opacity-0 scale-90"
x-transition:enter-end="opacity-100 scale-100"
@endif
x-transition:leave-end="opacity-0 scale-90"
class="relative flex duration-300 transform transition ease-in-out max-w-md w-full pointer-events-auto {{ $position->is('center') ? 'text-center' : 'text-left' }}"
:class="toast.select({ error: 'text-white', info: 'text-white', success: 'text-white', warning: 'text-white' })"
>
<i class=" flex items-center gap-2 select-none not-italic pr-6 pl-4 py-3 rounded shadow-lg w-full {{ $alignment->is('bottom') ? 'mt-3' : 'mb-3' }}"
:class="toast.select({
error: 'bg-coolgray-300',
info: 'bg-coolgray-300',
success: 'bg-coolgray-300',
warning: 'bg-coolgray-300'
})">
<template x-if="toast.type === 'success'">
<div class="rounded text-success">
<svg aria-hidden="true" class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"></path>
</svg>
</div>
</template>
<template x-if="toast.type === 'error'">
<div class="rounded text-error">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M12 9v4" />
<path d="M12 16v.01" />
</svg>
</div>
</template>
<template x-if="toast.type === 'info'">
<div class="rounded text-info">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
<path d="M12 9h.01" />
<path d="M11 12h1v4h1" />
</svg>
</div>
</template>
<template x-if="toast.type === 'warning'">
<div class="rounded text-warning">
<svg aria-hidden="true" class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
</div>
</template>
<span x-html="toast.message" class="w-full pr-10 break-words" />
</i>
@if ($closeable)
<button @click="toast.dispose()" aria-label="@lang('close')"
class="absolute right-0 p-4 focus:outline-none hover:bg-transparent/10 rounded {{ $alignment->is('bottom') ? 'top-3' : 'top-0' }}">
<svg aria-hidden="true" class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</button>
@endif
</div>
</template>
</div>

View File

@@ -6,7 +6,9 @@ use App\Actions\Database\StartMysql;
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;
use App\Providers\RouteServiceProvider;
use Illuminate\Http\Request;
@@ -32,105 +34,25 @@ if (isDev()) {
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', [Deploy::class, 'deploy']);
});
Route::middleware(['throttle:5'])->group(function () {

View File

@@ -67,6 +67,10 @@ use App\Livewire\Server\Proxy\Logs as ProxyLogs;
use App\Livewire\Source\Github\Change as GitHubChange;
use App\Livewire\Subscription\Index as SubscriptionIndex;
use App\Livewire\Tags\Index as TagsIndex;
use App\Livewire\Tags\Show as TagsShow;
use App\Livewire\TeamSharedVariablesIndex;
use App\Livewire\Waitlist\Index as WaitlistIndex;
@@ -106,7 +110,10 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/settings/license', SettingsLicense::class)->name('settings.license');
Route::get('/profile', ProfileIndex::class)->name('profile');
Route::prefix('tags')->group(function () {
Route::get('/', TagsIndex::class)->name('tags.index');
Route::get('/{tag_name}', TagsShow::class)->name('tags.show');
});
Route::prefix('team')->group(function () {
Route::get('/', TeamIndex::class)->name('team.index');
Route::get('/new', TeamCreate::class)->name('team.create');

View File

@@ -4,7 +4,7 @@
services:
filebrowser:
image: filebrowser/filebrowser:latest
image: filebrowser/filebrowser:s6
environment:
- SERVICE_FQDN_FILEBROWSER
- PUID=1000

View File

@@ -143,7 +143,7 @@
"filebrowser": {
"documentation": "https:\/\/filebrowser.org\/configuration",
"slogan": "FileBrowser is a self-hosted, web-based file manager and file explorer with a user-friendly interface. It allows you to manage and organize your files and directories directly from your web browser.",
"compose": "c2VydmljZXM6CiAgZmlsZWJyb3dzZXI6CiAgICBpbWFnZTogJ2ZpbGVicm93c2VyL2ZpbGVicm93c2VyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GSUxFQlJPV1NFUgogICAgICAtIFBVSUQ9MTAwMAogICAgICAtIFBHSUQ9MTAwMAogICAgdm9sdW1lczoKICAgICAgLSAnZmlsZWJyb3dzZXItc3J2Oi9zcnYnCiAgICAgIC0gJ2ZpbGVicm93c2VyLWRhdGFiYXNlOi9kYXRhYmFzZS9maWxlYnJvd3Nlci5kYicKICAgICAgLSAnZmlsZWJyb3dzZXItY29uZmlnOi9jb25maWcvc2V0dGluZ3MuanNvbicKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo4MCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=",
"compose": "c2VydmljZXM6CiAgZmlsZWJyb3dzZXI6CiAgICBpbWFnZTogJ2ZpbGVicm93c2VyL2ZpbGVicm93c2VyOnM2JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0ZJTEVCUk9XU0VSCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICB2b2x1bWVzOgogICAgICAtICdmaWxlYnJvd3Nlci1zcnY6L3NydicKICAgICAgLSAnZmlsZWJyb3dzZXItZGF0YWJhc2U6L2RhdGFiYXNlL2ZpbGVicm93c2VyLmRiJwogICAgICAtICdmaWxlYnJvd3Nlci1jb25maWc6L2NvbmZpZy9zZXR0aW5ncy5qc29uJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==",
"tags": [
"file-management",
"storage-access",

View File

@@ -4,7 +4,7 @@
"version": "3.12.36"
},
"v4": {
"version": "4.0.0-beta.203"
"version": "4.0.0-beta.205"
}
}
}