mirror of
https://github.com/ershisan99/coolify.git
synced 2026-01-08 12:34:01 +00:00
Compare commits
19 Commits
v4.0.0-bet
...
v4.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e3dc474f2 | ||
|
|
d3eb87561e | ||
|
|
8b58c8f856 | ||
|
|
8c60ef5bd6 | ||
|
|
1d59383c78 | ||
|
|
60f590454d | ||
|
|
dcb61a553e | ||
|
|
e06e31642f | ||
|
|
9dfce48380 | ||
|
|
8eed87e2f7 | ||
|
|
d56d4eb8fc | ||
|
|
fd32cd04ab | ||
|
|
1d3b7ffd3b | ||
|
|
0b5baf60a5 | ||
|
|
bc31df6fb2 | ||
|
|
818399bc23 | ||
|
|
e7fdff0f69 | ||
|
|
6312c0ba84 | ||
|
|
44efe0b5e1 |
@@ -45,6 +45,7 @@ class DeleteService
|
|||||||
foreach ($service->databases()->get() as $database) {
|
foreach ($service->databases()->get() as $database) {
|
||||||
$database->forceDelete();
|
$database->forceDelete();
|
||||||
}
|
}
|
||||||
|
$service->tags()->detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
159
app/Http/Controllers/Api/Deploy.php
Normal file
159
app/Http/Controllers/Api/Deploy.php
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
$applications = $found_tag->applications();
|
||||||
|
$services = $found_tag->services();
|
||||||
|
if ($applications->count() === 0 && $services->count() === 0) {
|
||||||
|
$message->push("No resources found for tag {$tag}.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach ($applications as $resource) {
|
||||||
|
$return_message = $this->deploy_resource($resource, $force);
|
||||||
|
$message = $message->merge($return_message);
|
||||||
|
}
|
||||||
|
foreach ($services 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -166,10 +166,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
|||||||
|
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
$this->application_deployment_queue->update([
|
|
||||||
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Generate custom host<->ip mapping
|
// Generate custom host<->ip mapping
|
||||||
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
|
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
|
||||||
if (!is_null($allContainers)) {
|
if (!is_null($allContainers)) {
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ class CloneMe extends Component
|
|||||||
public ?int $selectedDestination = null;
|
public ?int $selectedDestination = null;
|
||||||
public ?Server $server = null;
|
public ?Server $server = null;
|
||||||
public $resources = [];
|
public $resources = [];
|
||||||
public string $newProjectName = '';
|
public string $newName = '';
|
||||||
|
|
||||||
protected $messages = [
|
protected $messages = [
|
||||||
'selectedServer' => 'Please select a server.',
|
'selectedServer' => 'Please select a server.',
|
||||||
'selectedDestination' => 'Please select a server & destination.',
|
'selectedDestination' => 'Please select a server & destination.',
|
||||||
'newProjectName' => 'Please enter a name for the new project.',
|
'newName' => 'Please enter a name for the new project or environment.',
|
||||||
];
|
];
|
||||||
public function mount($project_uuid)
|
public function mount($project_uuid)
|
||||||
{
|
{
|
||||||
@@ -36,7 +36,7 @@ class CloneMe extends Component
|
|||||||
$this->environment = $this->project->environments->where('name', $this->environment_name)->first();
|
$this->environment = $this->project->environments->where('name', $this->environment_name)->first();
|
||||||
$this->project_id = $this->project->id;
|
$this->project_id = $this->project->id;
|
||||||
$this->servers = currentTeam()->servers;
|
$this->servers = currentTeam()->servers;
|
||||||
$this->newProjectName = str($this->project->name . '-clone-' . (string)new Cuid2(7))->slug();
|
$this->newName = str($this->project->name . '-clone-' . (string)new Cuid2(7))->slug();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
@@ -46,34 +46,50 @@ class CloneMe extends Component
|
|||||||
|
|
||||||
public function selectServer($server_id, $destination_id)
|
public function selectServer($server_id, $destination_id)
|
||||||
{
|
{
|
||||||
|
if ($server_id == $this->selectedServer && $destination_id == $this->selectedDestination) {
|
||||||
|
$this->selectedServer = null;
|
||||||
|
$this->selectedDestination = null;
|
||||||
|
$this->server = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
$this->selectedServer = $server_id;
|
$this->selectedServer = $server_id;
|
||||||
$this->selectedDestination = $destination_id;
|
$this->selectedDestination = $destination_id;
|
||||||
$this->server = $this->servers->where('id', $server_id)->first();
|
$this->server = $this->servers->where('id', $server_id)->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function clone()
|
public function clone(string $type)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->validate([
|
$this->validate([
|
||||||
'selectedDestination' => 'required',
|
'selectedDestination' => 'required',
|
||||||
'newProjectName' => 'required',
|
'newName' => 'required',
|
||||||
]);
|
]);
|
||||||
$foundProject = Project::where('name', $this->newProjectName)->first();
|
if ($type === 'project') {
|
||||||
if ($foundProject) {
|
$foundProject = Project::where('name', $this->newName)->first();
|
||||||
throw new \Exception('Project with the same name already exists.');
|
if ($foundProject) {
|
||||||
}
|
throw new \Exception('Project with the same name already exists.');
|
||||||
$newProject = Project::create([
|
}
|
||||||
'name' => $this->newProjectName,
|
$project = Project::create([
|
||||||
'team_id' => currentTeam()->id,
|
'name' => $this->newName,
|
||||||
'description' => $this->project->description . ' (clone)',
|
'team_id' => currentTeam()->id,
|
||||||
]);
|
'description' => $this->project->description . ' (clone)',
|
||||||
if ($this->environment->name !== 'production') {
|
]);
|
||||||
$newProject->environments()->create([
|
if ($this->environment->name !== 'production') {
|
||||||
'name' => $this->environment->name,
|
$project->environments()->create([
|
||||||
|
'name' => $this->environment->name,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
$environment = $project->environments->where('name', $this->environment->name)->first();
|
||||||
|
} else {
|
||||||
|
$foundEnv = $this->project->environments()->where('name', $this->newName)->first();
|
||||||
|
if ($foundEnv) {
|
||||||
|
throw new \Exception('Environment with the same name already exists.');
|
||||||
|
}
|
||||||
|
$project = $this->project;
|
||||||
|
$environment = $this->project->environments()->create([
|
||||||
|
'name' => $this->newName,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
$newEnvironment = $newProject->environments->where('name', $this->environment->name)->first();
|
|
||||||
// Clone Applications
|
|
||||||
$applications = $this->environment->applications;
|
$applications = $this->environment->applications;
|
||||||
$databases = $this->environment->databases();
|
$databases = $this->environment->databases();
|
||||||
$services = $this->environment->services;
|
$services = $this->environment->services;
|
||||||
@@ -83,7 +99,7 @@ class CloneMe extends Component
|
|||||||
'uuid' => $uuid,
|
'uuid' => $uuid,
|
||||||
'fqdn' => generateFqdn($this->server, $uuid),
|
'fqdn' => generateFqdn($this->server, $uuid),
|
||||||
'status' => 'exited',
|
'status' => 'exited',
|
||||||
'environment_id' => $newEnvironment->id,
|
'environment_id' => $environment->id,
|
||||||
// This is not correct, but we need to set it to something
|
// This is not correct, but we need to set it to something
|
||||||
'destination_id' => $this->selectedDestination,
|
'destination_id' => $this->selectedDestination,
|
||||||
]);
|
]);
|
||||||
@@ -110,7 +126,7 @@ class CloneMe extends Component
|
|||||||
'uuid' => $uuid,
|
'uuid' => $uuid,
|
||||||
'status' => 'exited',
|
'status' => 'exited',
|
||||||
'started_at' => null,
|
'started_at' => null,
|
||||||
'environment_id' => $newEnvironment->id,
|
'environment_id' => $environment->id,
|
||||||
'destination_id' => $this->selectedDestination,
|
'destination_id' => $this->selectedDestination,
|
||||||
]);
|
]);
|
||||||
$newDatabase->save();
|
$newDatabase->save();
|
||||||
@@ -136,7 +152,7 @@ class CloneMe extends Component
|
|||||||
$uuid = (string)new Cuid2(7);
|
$uuid = (string)new Cuid2(7);
|
||||||
$newService = $service->replicate()->fill([
|
$newService = $service->replicate()->fill([
|
||||||
'uuid' => $uuid,
|
'uuid' => $uuid,
|
||||||
'environment_id' => $newEnvironment->id,
|
'environment_id' => $environment->id,
|
||||||
'destination_id' => $this->selectedDestination,
|
'destination_id' => $this->selectedDestination,
|
||||||
]);
|
]);
|
||||||
$newService->save();
|
$newService->save();
|
||||||
@@ -153,8 +169,8 @@ class CloneMe extends Component
|
|||||||
$newService->parse();
|
$newService->parse();
|
||||||
}
|
}
|
||||||
return redirect()->route('project.resource.index', [
|
return redirect()->route('project.resource.index', [
|
||||||
'project_uuid' => $newProject->uuid,
|
'project_uuid' => $project->uuid,
|
||||||
'environment_name' => $newEnvironment->name,
|
'environment_name' => $environment->name,
|
||||||
]);
|
]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Models\PrivateKey;
|
|||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use App\Models\StandaloneDocker;
|
use App\Models\StandaloneDocker;
|
||||||
use App\Models\SwarmDocker;
|
use App\Models\SwarmDocker;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Spatie\Url\Url;
|
use Spatie\Url\Url;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@@ -18,7 +19,7 @@ class GithubPrivateRepositoryDeployKey extends Component
|
|||||||
public $current_step = 'private_keys';
|
public $current_step = 'private_keys';
|
||||||
public $parameters;
|
public $parameters;
|
||||||
public $query;
|
public $query;
|
||||||
public $private_keys;
|
public $private_keys =[];
|
||||||
public int $private_key_id;
|
public int $private_key_id;
|
||||||
|
|
||||||
public int $port = 3000;
|
public int $port = 3000;
|
||||||
@@ -33,6 +34,11 @@ class GithubPrivateRepositoryDeployKey extends Component
|
|||||||
public $build_pack = 'nixpacks';
|
public $build_pack = 'nixpacks';
|
||||||
public bool $show_is_static = true;
|
public bool $show_is_static = true;
|
||||||
|
|
||||||
|
private object $repository_url_parsed;
|
||||||
|
private GithubApp|GitlabApp|string $git_source = 'other';
|
||||||
|
private ?string $git_host = null;
|
||||||
|
private string $git_repository;
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'repository_url' => 'required',
|
'repository_url' => 'required',
|
||||||
'branch' => 'required|string',
|
'branch' => 'required|string',
|
||||||
@@ -49,10 +55,7 @@ class GithubPrivateRepositoryDeployKey extends Component
|
|||||||
'publish_directory' => 'Publish directory',
|
'publish_directory' => 'Publish directory',
|
||||||
'build_pack' => 'Build pack',
|
'build_pack' => 'Build pack',
|
||||||
];
|
];
|
||||||
private object $repository_url_parsed;
|
|
||||||
private GithubApp|GitlabApp|string $git_source = 'other';
|
|
||||||
private ?string $git_host = null;
|
|
||||||
private string $git_repository;
|
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class Index extends Component
|
|||||||
}
|
}
|
||||||
$this->project = $project;
|
$this->project = $project;
|
||||||
$this->environment = $environment;
|
$this->environment = $environment;
|
||||||
$this->applications = $environment->applications->sortBy('name');
|
$this->applications = $environment->applications->load(['tags']);
|
||||||
$this->applications = $this->applications->map(function ($application) {
|
$this->applications = $this->applications->map(function ($application) {
|
||||||
if (data_get($application, 'environment.project.uuid')) {
|
if (data_get($application, 'environment.project.uuid')) {
|
||||||
$application->hrefLink = route('project.application.configuration', [
|
$application->hrefLink = route('project.application.configuration', [
|
||||||
@@ -40,8 +40,9 @@ class Index extends Component
|
|||||||
}
|
}
|
||||||
return $application;
|
return $application;
|
||||||
});
|
});
|
||||||
$this->postgresqls = $environment->postgresqls->sortBy('name');
|
ray($this->applications);
|
||||||
$this->postgresqls = $this->postgresqls->map(function ($postgresql) {
|
$this->postgresqls = $environment->postgresqls->load(['tags'])->sortBy('name');
|
||||||
|
$this->postgresqls = $this->postgresqls->map(function ($postgresql) {
|
||||||
if (data_get($postgresql, 'environment.project.uuid')) {
|
if (data_get($postgresql, 'environment.project.uuid')) {
|
||||||
$postgresql->hrefLink = route('project.database.configuration', [
|
$postgresql->hrefLink = route('project.database.configuration', [
|
||||||
'project_uuid' => data_get($postgresql, 'environment.project.uuid'),
|
'project_uuid' => data_get($postgresql, 'environment.project.uuid'),
|
||||||
@@ -51,7 +52,7 @@ class Index extends Component
|
|||||||
}
|
}
|
||||||
return $postgresql;
|
return $postgresql;
|
||||||
});
|
});
|
||||||
$this->redis = $environment->redis->sortBy('name');
|
$this->redis = $environment->redis->load(['tags'])->sortBy('name');
|
||||||
$this->redis = $this->redis->map(function ($redis) {
|
$this->redis = $this->redis->map(function ($redis) {
|
||||||
if (data_get($redis, 'environment.project.uuid')) {
|
if (data_get($redis, 'environment.project.uuid')) {
|
||||||
$redis->hrefLink = route('project.database.configuration', [
|
$redis->hrefLink = route('project.database.configuration', [
|
||||||
@@ -62,7 +63,7 @@ class Index extends Component
|
|||||||
}
|
}
|
||||||
return $redis;
|
return $redis;
|
||||||
});
|
});
|
||||||
$this->mongodbs = $environment->mongodbs->sortBy('name');
|
$this->mongodbs = $environment->mongodbs->load(['tags'])->sortBy('name');
|
||||||
$this->mongodbs = $this->mongodbs->map(function ($mongodb) {
|
$this->mongodbs = $this->mongodbs->map(function ($mongodb) {
|
||||||
if (data_get($mongodb, 'environment.project.uuid')) {
|
if (data_get($mongodb, 'environment.project.uuid')) {
|
||||||
$mongodb->hrefLink = route('project.database.configuration', [
|
$mongodb->hrefLink = route('project.database.configuration', [
|
||||||
@@ -73,7 +74,7 @@ class Index extends Component
|
|||||||
}
|
}
|
||||||
return $mongodb;
|
return $mongodb;
|
||||||
});
|
});
|
||||||
$this->mysqls = $environment->mysqls->sortBy('name');
|
$this->mysqls = $environment->mysqls->load(['tags'])->sortBy('name');
|
||||||
$this->mysqls = $this->mysqls->map(function ($mysql) {
|
$this->mysqls = $this->mysqls->map(function ($mysql) {
|
||||||
if (data_get($mysql, 'environment.project.uuid')) {
|
if (data_get($mysql, 'environment.project.uuid')) {
|
||||||
$mysql->hrefLink = route('project.database.configuration', [
|
$mysql->hrefLink = route('project.database.configuration', [
|
||||||
@@ -84,7 +85,7 @@ class Index extends Component
|
|||||||
}
|
}
|
||||||
return $mysql;
|
return $mysql;
|
||||||
});
|
});
|
||||||
$this->mariadbs = $environment->mariadbs->sortBy('name');
|
$this->mariadbs = $environment->mariadbs->load(['tags'])->sortBy('name');
|
||||||
$this->mariadbs = $this->mariadbs->map(function ($mariadb) {
|
$this->mariadbs = $this->mariadbs->map(function ($mariadb) {
|
||||||
if (data_get($mariadb, 'environment.project.uuid')) {
|
if (data_get($mariadb, 'environment.project.uuid')) {
|
||||||
$mariadb->hrefLink = route('project.database.configuration', [
|
$mariadb->hrefLink = route('project.database.configuration', [
|
||||||
@@ -95,7 +96,7 @@ class Index extends Component
|
|||||||
}
|
}
|
||||||
return $mariadb;
|
return $mariadb;
|
||||||
});
|
});
|
||||||
$this->services = $environment->services->sortBy('name');
|
$this->services = $environment->services->load(['tags'])->sortBy('name');
|
||||||
$this->services = $this->services->map(function ($service) {
|
$this->services = $this->services->map(function ($service) {
|
||||||
if (data_get($service, 'environment.project.uuid')) {
|
if (data_get($service, 'environment.project.uuid')) {
|
||||||
$service->hrefLink = route('project.service.configuration', [
|
$service->hrefLink = route('project.service.configuration', [
|
||||||
|
|||||||
90
app/Livewire/Project/Shared/Tags.php
Normal file
90
app/Livewire/Project/Shared/Tags.php
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<?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(string $id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->resource->tags()->detach($id);
|
||||||
|
|
||||||
|
$found_more_tags = Tag::where(['id' => $id, 'team_id' => currentTeam()->id])->first();
|
||||||
|
if ($found_more_tags->applications()->count() == 0 && $found_more_tags->services()->count() == 0){
|
||||||
|
$found_more_tags->delete();
|
||||||
|
}
|
||||||
|
$this->refresh();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return handleError($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public function refresh()
|
||||||
|
{
|
||||||
|
$this->resource->load(['tags']);
|
||||||
|
$this->tags = Tag::ownedByCurrentTeam()->get();
|
||||||
|
$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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,7 +39,7 @@ class ShowPrivateKey extends Component
|
|||||||
if ($uptime) {
|
if ($uptime) {
|
||||||
$this->dispatch('success', 'Server is reachable.');
|
$this->dispatch('success', 'Server is reachable.');
|
||||||
} else {
|
} else {
|
||||||
$this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/configuration#openssh-server">documentation</a> for further help.');
|
$this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh#openssh">documentation</a> for further help.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|||||||
18
app/Livewire/Tags/Index.php
Normal file
18
app/Livewire/Tags/Index.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
71
app/Livewire/Tags/Show.php
Normal file
71
app/Livewire/Tags/Show.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?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 $tags;
|
||||||
|
public Tag $tag;
|
||||||
|
public $applications;
|
||||||
|
public $services;
|
||||||
|
public $webhook = null;
|
||||||
|
public $deployments_per_tag_per_server = [];
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name');
|
||||||
|
$tag = $this->tags->where('name', request()->tag_name)->first();
|
||||||
|
if (!$tag) {
|
||||||
|
return redirect()->route('tags.index');
|
||||||
|
}
|
||||||
|
$this->webhook = generatTagDeployWebhook($tag->name);
|
||||||
|
$this->applications = $tag->applications()->get();
|
||||||
|
$this->services = $tag->services()->get();
|
||||||
|
$this->tag = $tag;
|
||||||
|
$this->get_deployments();
|
||||||
|
}
|
||||||
|
public function get_deployments()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$resource_ids = $this->applications->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->applications->each(function ($resource) {
|
||||||
|
$deploy = new Deploy();
|
||||||
|
$deploy->deploy_resource($resource);
|
||||||
|
});
|
||||||
|
$this->services->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 render()
|
||||||
|
{
|
||||||
|
return view('livewire.tags.show');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,7 @@ class Application extends BaseModel
|
|||||||
$application->persistentStorages()->delete();
|
$application->persistentStorages()->delete();
|
||||||
$application->environment_variables()->delete();
|
$application->environment_variables()->delete();
|
||||||
$application->environment_variables_preview()->delete();
|
$application->environment_variables_preview()->delete();
|
||||||
|
$application->tags()->detach();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,6 +212,13 @@ class Application extends BaseModel
|
|||||||
: explode(',', $this->ports_exposes)
|
: explode(',', $this->ports_exposes)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
public function tags()
|
||||||
|
{
|
||||||
|
return $this->morphToMany(Tag::class, 'taggable');
|
||||||
|
}
|
||||||
|
public function project() {
|
||||||
|
return data_get($this, 'environment.project');
|
||||||
|
}
|
||||||
public function team()
|
public function team()
|
||||||
{
|
{
|
||||||
return data_get($this, 'environment.project.team');
|
return data_get($this, 'environment.project.team');
|
||||||
|
|||||||
@@ -16,10 +16,18 @@ class Service extends BaseModel
|
|||||||
{
|
{
|
||||||
return 'service';
|
return 'service';
|
||||||
}
|
}
|
||||||
|
public function project()
|
||||||
|
{
|
||||||
|
return data_get($this, 'environment.project');
|
||||||
|
}
|
||||||
public function team()
|
public function team()
|
||||||
{
|
{
|
||||||
return data_get($this, 'environment.project.team');
|
return data_get($this, 'environment.project.team');
|
||||||
}
|
}
|
||||||
|
public function tags()
|
||||||
|
{
|
||||||
|
return $this->morphToMany(Tag::class, 'taggable');
|
||||||
|
}
|
||||||
public function extraFields()
|
public function extraFields()
|
||||||
{
|
{
|
||||||
$fields = collect([]);
|
$fields = collect([]);
|
||||||
|
|||||||
@@ -40,8 +40,14 @@ class StandaloneMariadb extends BaseModel
|
|||||||
$database->scheduledBackups()->delete();
|
$database->scheduledBackups()->delete();
|
||||||
$database->persistentStorages()->delete();
|
$database->persistentStorages()->delete();
|
||||||
$database->environment_variables()->delete();
|
$database->environment_variables()->delete();
|
||||||
|
$database->tags()->detach();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function tags()
|
||||||
|
{
|
||||||
|
return $this->morphToMany(Tag::class, 'taggable');
|
||||||
|
}
|
||||||
public function team()
|
public function team()
|
||||||
{
|
{
|
||||||
return data_get($this, 'environment.project.team');
|
return data_get($this, 'environment.project.team');
|
||||||
|
|||||||
@@ -43,8 +43,14 @@ class StandaloneMongodb extends BaseModel
|
|||||||
$database->scheduledBackups()->delete();
|
$database->scheduledBackups()->delete();
|
||||||
$database->persistentStorages()->delete();
|
$database->persistentStorages()->delete();
|
||||||
$database->environment_variables()->delete();
|
$database->environment_variables()->delete();
|
||||||
|
$database->tags()->detach();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function tags()
|
||||||
|
{
|
||||||
|
return $this->morphToMany(Tag::class, 'taggable');
|
||||||
|
}
|
||||||
public function team()
|
public function team()
|
||||||
{
|
{
|
||||||
return data_get($this, 'environment.project.team');
|
return data_get($this, 'environment.project.team');
|
||||||
|
|||||||
@@ -40,8 +40,14 @@ class StandaloneMysql extends BaseModel
|
|||||||
$database->scheduledBackups()->delete();
|
$database->scheduledBackups()->delete();
|
||||||
$database->persistentStorages()->delete();
|
$database->persistentStorages()->delete();
|
||||||
$database->environment_variables()->delete();
|
$database->environment_variables()->delete();
|
||||||
|
$database->tags()->detach();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function tags()
|
||||||
|
{
|
||||||
|
return $this->morphToMany(Tag::class, 'taggable');
|
||||||
|
}
|
||||||
public function team()
|
public function team()
|
||||||
{
|
{
|
||||||
return data_get($this, 'environment.project.team');
|
return data_get($this, 'environment.project.team');
|
||||||
|
|||||||
@@ -40,8 +40,14 @@ class StandalonePostgresql extends BaseModel
|
|||||||
$database->scheduledBackups()->delete();
|
$database->scheduledBackups()->delete();
|
||||||
$database->persistentStorages()->delete();
|
$database->persistentStorages()->delete();
|
||||||
$database->environment_variables()->delete();
|
$database->environment_variables()->delete();
|
||||||
|
$database->tags()->detach();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function tags()
|
||||||
|
{
|
||||||
|
return $this->morphToMany(Tag::class, 'taggable');
|
||||||
|
}
|
||||||
public function link()
|
public function link()
|
||||||
{
|
{
|
||||||
if (data_get($this, 'environment.project.uuid')) {
|
if (data_get($this, 'environment.project.uuid')) {
|
||||||
|
|||||||
@@ -35,8 +35,14 @@ class StandaloneRedis extends BaseModel
|
|||||||
}
|
}
|
||||||
$database->persistentStorages()->delete();
|
$database->persistentStorages()->delete();
|
||||||
$database->environment_variables()->delete();
|
$database->environment_variables()->delete();
|
||||||
|
$database->tags()->detach();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function tags()
|
||||||
|
{
|
||||||
|
return $this->morphToMany(Tag::class, 'taggable');
|
||||||
|
}
|
||||||
public function team()
|
public function team()
|
||||||
{
|
{
|
||||||
return data_get($this, 'environment.project.team');
|
return data_get($this, 'environment.project.team');
|
||||||
|
|||||||
31
app/Models/Tag.php
Normal file
31
app/Models/Tag.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?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 services()
|
||||||
|
{
|
||||||
|
return $this->morphedByMany(Service::class, 'taggable');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Enums\ApplicationDeploymentStatus;
|
||||||
use App\Jobs\ApplicationDeploymentJob;
|
use App\Jobs\ApplicationDeploymentJob;
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
use App\Models\ApplicationDeploymentQueue;
|
use App\Models\ApplicationDeploymentQueue;
|
||||||
@@ -29,6 +30,9 @@ function queue_application_deployment(Application $application, string $deployme
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (next_queuable($server_id, $application_id)) {
|
if (next_queuable($server_id, $application_id)) {
|
||||||
|
$deployment->update([
|
||||||
|
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
|
||||||
|
]);
|
||||||
dispatch(new ApplicationDeploymentJob(
|
dispatch(new ApplicationDeploymentJob(
|
||||||
application_deployment_queue_id: $deployment->id,
|
application_deployment_queue_id: $deployment->id,
|
||||||
));
|
));
|
||||||
@@ -40,6 +44,9 @@ function queue_next_deployment(Application $application)
|
|||||||
$server_id = $application->destination->server_id;
|
$server_id = $application->destination->server_id;
|
||||||
$next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', 'queued')->get()->sortBy('created_at')->first();
|
$next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', 'queued')->get()->sortBy('created_at')->first();
|
||||||
if ($next_found) {
|
if ($next_found) {
|
||||||
|
$next_found->update([
|
||||||
|
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
|
||||||
|
]);
|
||||||
dispatch(new ApplicationDeploymentJob(
|
dispatch(new ApplicationDeploymentJob(
|
||||||
application_deployment_queue_id: $next_found->id,
|
application_deployment_queue_id: $next_found->id,
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -110,9 +110,9 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
|
|||||||
}
|
}
|
||||||
if ($error instanceof UniqueConstraintViolationException) {
|
if ($error instanceof UniqueConstraintViolationException) {
|
||||||
if (isset($livewire)) {
|
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) {
|
if ($error instanceof Throwable) {
|
||||||
@@ -481,7 +481,14 @@ function queryResourcesByUuid(string $uuid)
|
|||||||
if ($mariadb) return $mariadb;
|
if ($mariadb) return $mariadb;
|
||||||
return $resource;
|
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)
|
function generateDeployWebhook($resource)
|
||||||
{
|
{
|
||||||
$baseUrl = base_url();
|
$baseUrl = base_url();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ return [
|
|||||||
|
|
||||||
// The release version of your application
|
// The release version of your application
|
||||||
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
|
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
|
||||||
'release' => '4.0.0-beta.204',
|
'release' => '4.0.0-beta.206',
|
||||||
// When left empty or `null` the Laravel environment will be used
|
// When left empty or `null` the Laravel environment will be used
|
||||||
'environment' => config('app.env'),
|
'environment' => config('app.env'),
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
|
||||||
];
|
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return '4.0.0-beta.204';
|
return '4.0.0-beta.206';
|
||||||
|
|||||||
39
database/migrations/2024_02_01_111228_create_tags_table.php
Normal file
39
database/migrations/2024_02_01_111228_create_tags_table.php
Normal 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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -14,6 +14,10 @@ button[isError] {
|
|||||||
@apply bg-red-600 hover:bg-red-700;
|
@apply bg-red-600 hover:bg-red-700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button[isHighlighted] {
|
||||||
|
@apply bg-coollabs hover:bg-coollabs-100;
|
||||||
|
}
|
||||||
|
|
||||||
.scrollbar {
|
.scrollbar {
|
||||||
@apply scrollbar-thumb-coollabs-100 scrollbar-track-coolgray-200 scrollbar-w-2;
|
@apply scrollbar-thumb-coollabs-100 scrollbar-track-coolgray-200 scrollbar-w-2;
|
||||||
}
|
}
|
||||||
@@ -72,11 +76,11 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.box {
|
.box {
|
||||||
@apply flex p-2 transition-colors cursor-pointer min-h-[4rem] bg-coolgray-100 hover:bg-coollabs-100 hover:text-white hover:no-underline min-w-[24rem];
|
@apply flex p-2 transition-colors cursor-pointer min-h-[4rem] bg-coolgray-100 hover:bg-coollabs-100 hover:text-white hover:no-underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.box-without-bg {
|
.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 {
|
.description {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a>
|
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">
|
<ul class="flex flex-col h-full gap-4 menu flex-nowrap">
|
||||||
<li title="Dashboard">
|
<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' }}"
|
<svg xmlns="http://www.w3.org/2000/svg" class="{{ request()->is('/') ? 'text-warning icon' : 'icon' }}"
|
||||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li title="Servers">
|
<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"
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
class="{{ request()->is('server/*') || request()->is('servers') ? 'text-warning icon' : 'icon' }}"
|
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"
|
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li title="Projects">
|
<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"
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
class="{{ request()->is('project/*') || request()->is('projects') ? 'text-warning icon' : 'icon' }}"
|
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"
|
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li title="Command Center">
|
<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"
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
class="{{ request()->is('command-center') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24"
|
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"
|
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li title="Source">
|
<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">
|
<svg class="icon" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill="currentColor"
|
<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" />
|
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>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li title="Security">
|
<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">
|
<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"
|
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li title="Teams">
|
<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"
|
<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">
|
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
@@ -83,6 +83,18 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
<div class="flex-1"></div>
|
||||||
@if (isInstanceAdmin() && !isCloud())
|
@if (isInstanceAdmin() && !isCloud())
|
||||||
@@ -103,7 +115,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li title="Profile">
|
<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"
|
<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">
|
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
@@ -116,7 +128,7 @@
|
|||||||
|
|
||||||
@if (isInstanceAdmin())
|
@if (isInstanceAdmin())
|
||||||
<li title="Settings" class="mt-auto">
|
<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"
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
class="{{ request()->is('settings*') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24"
|
class="{{ request()->is('settings*') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24"
|
||||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||||
|
|||||||
@@ -4,15 +4,22 @@
|
|||||||
'isErrorButton' => false,
|
'isErrorButton' => false,
|
||||||
'disabled' => false,
|
'disabled' => false,
|
||||||
'action' => 'delete',
|
'action' => 'delete',
|
||||||
|
'content' => null,
|
||||||
])
|
])
|
||||||
<div x-data="{ modalOpen: false }" @keydown.escape.window="modalOpen = false" :class="{ 'z-40': modalOpen }"
|
<div x-data="{ modalOpen: false }" @keydown.escape.window="modalOpen = false" :class="{ 'z-40': modalOpen }"
|
||||||
class="relative w-auto h-auto">
|
class="relative w-auto h-auto">
|
||||||
@if ($disabled)
|
@if ($content)
|
||||||
<x-forms.button isError disabled>{{ $buttonTitle }}</x-forms.button>
|
<div @click="modalOpen=true">
|
||||||
@elseif ($isErrorButton)
|
{{ $content }}
|
||||||
<x-forms.button isError @click="modalOpen=true">{{ $buttonTitle }}</x-forms.button>
|
</div>
|
||||||
@else
|
@else
|
||||||
<x-forms.button @click="modalOpen=true">{{ $buttonTitle }}</x-forms.button>
|
@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
|
||||||
@endif
|
@endif
|
||||||
<template x-teleport="body">
|
<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"
|
<div x-show="modalOpen" class="fixed top-0 left-0 z-[99] flex items-center justify-center w-screen h-screen"
|
||||||
@@ -30,13 +37,13 @@
|
|||||||
class="relative w-full py-6 border rounded shadow-lg bg-coolgray-100 px-7 border-coolgray-300 sm:max-w-lg">
|
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">
|
<div class="flex items-center justify-between pb-3">
|
||||||
<h3 class="text-2xl font-bold">{{ $title }}</h3>
|
<h3 class="text-2xl font-bold">{{ $title }}</h3>
|
||||||
<button @click="modalOpen=false"
|
{{-- <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">
|
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"
|
<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">
|
stroke-width="1.5" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button> --}}
|
||||||
</div>
|
</div>
|
||||||
<div class="relative w-auto pb-8">
|
<div class="relative w-auto pb-8">
|
||||||
{{ $slot }}
|
{{ $slot }}
|
||||||
@@ -46,9 +53,15 @@
|
|||||||
type="button">Cancel
|
type="button">Cancel
|
||||||
</x-forms.button>
|
</x-forms.button>
|
||||||
<div class="flex-1"></div>
|
<div class="flex-1"></div>
|
||||||
<x-forms.button @click="modalOpen=false" class="w-24" isError type="button"
|
@if ($isErrorButton)
|
||||||
wire:click.prevent='{{ $action }}'>Continue
|
<x-forms.button @click="modalOpen=false" class="w-24" isError type="button"
|
||||||
</x-forms.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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,20 +22,7 @@
|
|||||||
if (typeof options.html != 'undefined') html = options.html;
|
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.dispatchEvent(new CustomEvent('toast-show', { detail: { type: type, message: message, description: description, position: position, html: html } }));
|
||||||
}
|
}" class="relative space-y-5">
|
||||||
|
|
||||||
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">
|
|
||||||
<template x-teleport="body">
|
<template x-teleport="body">
|
||||||
<ul x-data="{
|
<ul x-data="{
|
||||||
toasts: [],
|
toasts: [],
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<x-navbar-subscription />
|
<x-navbar-subscription />
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<main class="main max-w-screen-2xl">
|
<main class="mx-auto main max-w-screen-2xl">
|
||||||
{{ $slot }}
|
{{ $slot }}
|
||||||
</main>
|
</main>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -102,41 +102,40 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<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)
|
@if (count($deployments_per_server) > 0)
|
||||||
<x-loading />
|
<x-loading />
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
{{-- <div wire:poll.4000ms="get_deployments" class="grid grid-cols-1 gap-2 lg:grid-cols-3"> --}}
|
<div wire:poll.1000ms="get_deployments" class="grid grid-cols-1">
|
||||||
<div class="grid grid-cols-1">
|
|
||||||
@forelse ($deployments_per_server as $server_name => $deployments)
|
@forelse ($deployments_per_server as $server_name => $deployments)
|
||||||
<h4 class="py-4">{{ $server_name }}</h4>
|
<h4 class="py-4">{{ $server_name }}</h4>
|
||||||
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
|
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
|
||||||
@foreach ($deployments as $deployment)
|
@foreach ($deployments as $deployment)
|
||||||
<a href="{{ data_get($deployment, 'deployment_url') }}" @class([
|
<a href="{{ data_get($deployment, 'deployment_url') }}" @class([
|
||||||
'gap-2 cursor-pointer box group border-l-2 border-dotted',
|
'gap-2 cursor-pointer box group border-l-2 border-dotted',
|
||||||
'border-white' => data_get($deployment, 'status') === 'queued',
|
'border-coolgray-500' => data_get($deployment, 'status') === 'queued',
|
||||||
'border-yellow-500' => data_get($deployment, 'status') === 'in_progress',
|
'border-yellow-500' => data_get($deployment, 'status') === 'in_progress',
|
||||||
])>
|
])>
|
||||||
<div class="flex flex-col mx-6">
|
<div class="flex flex-col mx-6">
|
||||||
<div class="font-bold text-white">
|
<div class="font-bold text-white">
|
||||||
{{ data_get($deployment, 'application_name') }}
|
{{ data_get($deployment, 'application_name') }}
|
||||||
</div>
|
</div>
|
||||||
@if (data_get($deployment, 'pull_request_id') !== 0)
|
@if (data_get($deployment, 'pull_request_id') !== 0)
|
||||||
<div class="description">
|
<div class="description">
|
||||||
PR #{{ data_get($deployment, 'pull_request_id') }}
|
PR #{{ data_get($deployment, 'pull_request_id') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
<div class="description">
|
||||||
|
{{ str(data_get($deployment, 'status'))->headline() }}
|
||||||
</div>
|
</div>
|
||||||
@endif
|
|
||||||
<div class="description">
|
|
||||||
{{ str(data_get($deployment, 'status'))->headline() }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="flex-1"></div>
|
||||||
<div class="flex-1"></div>
|
</a>
|
||||||
</a>
|
@endforeach
|
||||||
@endforeach
|
|
||||||
</div>
|
</div>
|
||||||
@empty
|
@empty
|
||||||
<div>No queued / in progress deployments</div>
|
<div>No deployments running.</div>
|
||||||
@endforelse
|
@endforelse
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -63,6 +63,9 @@
|
|||||||
@click.prevent="activeTab = 'resource-operations'; window.location.hash = 'resource-operations'"
|
@click.prevent="activeTab = 'resource-operations'; window.location.hash = 'resource-operations'"
|
||||||
href="#">Resource Operations
|
href="#">Resource Operations
|
||||||
</a>
|
</a>
|
||||||
|
<a :class="activeTab === 'tags' && 'text-white'"
|
||||||
|
@click.prevent="activeTab = 'tags'; window.location.hash = 'tags'" href="#">Tags
|
||||||
|
</a>
|
||||||
<a :class="activeTab === 'danger' && 'text-white'"
|
<a :class="activeTab === 'danger' && 'text-white'"
|
||||||
@click.prevent="activeTab = 'danger'; window.location.hash = 'danger'" href="#">Danger Zone
|
@click.prevent="activeTab = 'danger'; window.location.hash = 'danger'" href="#">Danger Zone
|
||||||
</a>
|
</a>
|
||||||
@@ -112,6 +115,9 @@
|
|||||||
<div x-cloak x-show="activeTab === 'resource-operations'">
|
<div x-cloak x-show="activeTab === 'resource-operations'">
|
||||||
<livewire:project.shared.resource-operations :resource="$application" />
|
<livewire:project.shared.resource-operations :resource="$application" />
|
||||||
</div>
|
</div>
|
||||||
|
<div x-cloak x-show="activeTab === 'tags'">
|
||||||
|
<livewire:project.shared.tags :resource="$application" />
|
||||||
|
</div>
|
||||||
<div x-cloak x-show="activeTab === 'danger'">
|
<div x-cloak x-show="activeTab === 'danger'">
|
||||||
<livewire:project.shared.danger :resource="$application" />
|
<livewire:project.shared.danger :resource="$application" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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."
|
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" />
|
label="Docker Image Tag" />
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
<form wire:submit='clone'>
|
<form>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<h1>Clone</h1>
|
<h1>Clone</h1>
|
||||||
<div class="subtitle ">Quickly clone all resources to a new project</div>
|
<div class="subtitle ">Quickly clone all resources to a new project or environment</div>
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
|
<x-forms.input required id="newName" label="New Name" />
|
||||||
|
<x-forms.button isHighlighted wire:click="clone('project')" class="mt-4">Clone to a new Project</x-forms.button>
|
||||||
|
<x-forms.button isHighlighted wire:click="clone('environment')" class="mt-4">Clone to a new Environment</x-forms.button>
|
||||||
<h3 class="pt-4 pb-2">Servers</h3>
|
<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">
|
<div class="flex flex-col gap-4">
|
||||||
@foreach ($servers->sortBy('id') as $server)
|
@foreach ($servers->sortBy('id') as $server)
|
||||||
<div class="p-4 border border-coolgray-500">
|
<div class="p-4">
|
||||||
<h3>{{ $server->name }}</h3>
|
<h4>{{ $server->name }}</h4>
|
||||||
<h5>{{ $server->description }}</h5>
|
|
||||||
<div class="pt-4 pb-2">Docker Networks</div>
|
<div class="pt-4 pb-2">Docker Networks</div>
|
||||||
<div class="grid grid-cols-1 gap-2 pb-4 lg:grid-cols-4">
|
<div class="grid grid-cols-1 gap-2 pb-4 lg:grid-cols-4">
|
||||||
@foreach ($server->destinations() as $destination)
|
@foreach ($server->destinations() as $destination)
|
||||||
@@ -28,9 +27,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="pt-4 pb-2">Resources</h3>
|
<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 pt-4 opacity-95 lg:grid-cols-2 xl:grid-cols-3">
|
||||||
@foreach ($environment->applications->sortBy('name') as $application)
|
@foreach ($environment->applications->sortBy('name') as $application)
|
||||||
<div>
|
<div class="cursor-default box-without-bg bg-coolgray-100 group">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="font-bold text-white">{{ $application->name }}</div>
|
<div class="font-bold text-white">{{ $application->name }}</div>
|
||||||
<div class="description">{{ $application->description }}</div>
|
<div class="description">{{ $application->description }}</div>
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
@foreach ($environment->databases()->sortBy('name') as $database)
|
@foreach ($environment->databases()->sortBy('name') as $database)
|
||||||
<div>
|
<div class="cursor-default box-without-bg bg-coolgray-100 group">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="font-bold text-white">{{ $database->name }}</div>
|
<div class="font-bold text-white">{{ $database->name }}</div>
|
||||||
<div class="description">{{ $database->description }}</div>
|
<div class="description">{{ $database->description }}</div>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
@foreach ($environment->services->sortBy('name') as $service)
|
@foreach ($environment->services->sortBy('name') as $service)
|
||||||
<div>
|
<div class="cursor-default box-without-bg bg-coolgray-100 group">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="font-bold text-white">{{ $service->name }}</div>
|
<div class="font-bold text-white">{{ $service->name }}</div>
|
||||||
<div class="description">{{ $service->description }}</div>
|
<div class="description">{{ $service->description }}</div>
|
||||||
|
|||||||
@@ -48,6 +48,9 @@
|
|||||||
@click.prevent="activeTab = 'resource-operations'; window.location.hash = 'resource-operations'"
|
@click.prevent="activeTab = 'resource-operations'; window.location.hash = 'resource-operations'"
|
||||||
href="#">Resource Operations
|
href="#">Resource Operations
|
||||||
</a>
|
</a>
|
||||||
|
<a :class="activeTab === 'tags' && 'text-white'"
|
||||||
|
@click.prevent="activeTab = 'tags'; window.location.hash = 'tags'" href="#">Tags
|
||||||
|
</a>
|
||||||
<a :class="activeTab === 'danger' && 'text-white'"
|
<a :class="activeTab === 'danger' && 'text-white'"
|
||||||
@click.prevent="activeTab = 'danger';
|
@click.prevent="activeTab = 'danger';
|
||||||
window.location.hash = 'danger'"
|
window.location.hash = 'danger'"
|
||||||
@@ -89,6 +92,9 @@
|
|||||||
<div x-cloak x-show="activeTab === 'resource-operations'">
|
<div x-cloak x-show="activeTab === 'resource-operations'">
|
||||||
<livewire:project.shared.resource-operations :resource="$database" />
|
<livewire:project.shared.resource-operations :resource="$database" />
|
||||||
</div>
|
</div>
|
||||||
|
<div x-cloak x-show="activeTab === 'tags'">
|
||||||
|
<livewire:project.shared.tags :resource="$database" />
|
||||||
|
</div>
|
||||||
<div x-cloak x-show="activeTab === 'danger'">
|
<div x-cloak x-show="activeTab === 'danger'">
|
||||||
<livewire:project.shared.danger :resource="$database" />
|
<livewire:project.shared.danger :resource="$database" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<li class="step">Select a Repository, Branch & Save</li>
|
<li class="step">Select a Repository, Branch & Save</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="flex flex-col justify-center gap-2 text-left xl:flex-row">
|
<div class="flex flex-col justify-center gap-2 text-left xl:flex-row">
|
||||||
@foreach ($private_keys as $key)
|
@forelse ($private_keys as $key)
|
||||||
@if ($private_key_id == $key->id)
|
@if ($private_key_id == $key->id)
|
||||||
<div class="gap-2 py-4 cursor-pointer group hover:bg-coollabs bg-coolgray-200"
|
<div class="gap-2 py-4 cursor-pointer group hover:bg-coollabs bg-coolgray-200"
|
||||||
wire:click.defer="setPrivateKey('{{ $key->id }}')" wire:key="{{ $key->id }}">
|
wire:click.defer="setPrivateKey('{{ $key->id }}')" wire:key="{{ $key->id }}">
|
||||||
@@ -32,7 +32,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@endforeach
|
@empty
|
||||||
|
<div class="flex flex-col items-center justify-center gap-2">
|
||||||
|
<div class="text-neutral-500">
|
||||||
|
No private keys found.
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('security.private-key.index') }}">
|
||||||
|
<x-forms.button>Create a new private key</x-forms.button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
@endforelse
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@if ($current_step === 'repository')
|
@if ($current_step === 'repository')
|
||||||
|
|||||||
@@ -46,126 +46,197 @@
|
|||||||
@else
|
@else
|
||||||
<div x-data="searchComponent()">
|
<div x-data="searchComponent()">
|
||||||
<x-forms.input placeholder="Search for name, fqdn..." class="w-full" x-model="search" />
|
<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 grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3">
|
||||||
<template x-for="item in filteredApplications" :key="item.id">
|
<template x-for="item in filteredApplications" :key="item.id">
|
||||||
<a class="relative box group" :href="item.hrefLink">
|
<span class="relative">
|
||||||
<div class="flex flex-col mx-6">
|
<a class="h-24 box group" :href="item.hrefLink">
|
||||||
<div class="pb-2 font-bold text-white" x-text="item.name"></div>
|
<div class="flex flex-col mx-6">
|
||||||
<div class="description" x-text="item.description"></div>
|
<div class="pb-2 font-bold text-white" x-text="item.name"></div>
|
||||||
<div class="description" x-text="item.fqdn"></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>
|
</div>
|
||||||
<template x-if="item.status.startsWith('running')">
|
</span>
|
||||||
<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>
|
|
||||||
</template>
|
</template>
|
||||||
<template x-for="item in filteredPostgresqls" :key="item.id">
|
<template x-for="item in filteredPostgresqls" :key="item.id">
|
||||||
<a class="relative box group" :href="item.hrefLink">
|
<span class="relative">
|
||||||
<div class="flex flex-col mx-6">
|
<a class="h-24 box group" :href="item.hrefLink">
|
||||||
<div class="font-bold text-white" x-text="item.name"></div>
|
<div class="flex flex-col mx-6">
|
||||||
<div class="description" x-text="item.description"></div>
|
<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>
|
</div>
|
||||||
<template x-if="item.status.startsWith('running')">
|
</span>
|
||||||
<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>
|
|
||||||
</template>
|
</template>
|
||||||
<template x-for="item in filteredRedis" :key="item.id">
|
<template x-for="item in filteredRedis" :key="item.id">
|
||||||
<a class="relative box group" :href="item.hrefLink">
|
<span class="relative">
|
||||||
<div class="flex flex-col mx-6">
|
<a class="h-24 box group" :href="item.hrefLink">
|
||||||
<div class="font-bold text-white" x-text="item.name"></div>
|
<div class="flex flex-col mx-6">
|
||||||
<div class="description" x-text="item.description"></div>
|
<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>
|
</div>
|
||||||
<template x-if="item.status.startsWith('running')">
|
</span>
|
||||||
<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>
|
|
||||||
</template>
|
</template>
|
||||||
<template x-for="item in filteredMongodbs" :key="item.id">
|
<template x-for="item in filteredMongodbs" :key="item.id">
|
||||||
<a class="relative box group" :href="item.hrefLink">
|
<span class="relative">
|
||||||
<div class="flex flex-col mx-6">
|
<a class="h-24 box group" :href="item.hrefLink">
|
||||||
<div class="font-bold text-white" x-text="item.name"></div>
|
<div class="flex flex-col mx-6">
|
||||||
<div class="description" x-text="item.description"></div>
|
<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>
|
</div>
|
||||||
<template x-if="item.status.startsWith('running')">
|
</span>
|
||||||
<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>
|
|
||||||
</template>
|
</template>
|
||||||
<template x-for="item in filteredMysqls" :key="item.id">
|
<template x-for="item in filteredMysqls" :key="item.id">
|
||||||
<a class="relative box group" :href="item.hrefLink">
|
<span class="relative">
|
||||||
<div class="flex flex-col mx-6">
|
<a class="h-24 box group" :href="item.hrefLink">
|
||||||
<div class="font-bold text-white" x-text="item.name"></div>
|
<div class="flex flex-col mx-6">
|
||||||
<div class="description" x-text="item.description"></div>
|
<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>
|
</div>
|
||||||
<template x-if="item.status.startsWith('running')">
|
</span>
|
||||||
<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>
|
|
||||||
</template>
|
</template>
|
||||||
<template x-for="item in filteredMariadbs" :key="item.id">
|
<template x-for="item in filteredMariadbs" :key="item.id">
|
||||||
<a class="relative box group" :href="item.hrefLink">
|
<span class="relative">
|
||||||
<div class="flex flex-col mx-6">
|
<a class="h-24 box group" :href="item.hrefLink">
|
||||||
<div class="font-bold text-white" x-text="item.name"></div>
|
<div class="flex flex-col mx-6">
|
||||||
<div class="description" x-text="item.description"></div>
|
<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>
|
</div>
|
||||||
<template x-if="item.status.startsWith('running')">
|
</span>
|
||||||
<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>
|
|
||||||
</template>
|
</template>
|
||||||
<template x-for="item in filteredServices" :key="item.id">
|
<template x-for="item in filteredServices" :key="item.id">
|
||||||
<a class="relative box group" :href="item.hrefLink">
|
<span class="relative">
|
||||||
<div class="flex flex-col mx-6">
|
<a class="h-24 box group" :href="item.hrefLink">
|
||||||
<div class="font-bold text-white" x-text="item.name"></div>
|
<div class="flex flex-col mx-6">
|
||||||
<div class="description" x-text="item.description"></div>
|
<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>
|
</div>
|
||||||
<template x-if="item.status.startsWith('running')">
|
</span>
|
||||||
<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>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -174,6 +245,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
function sortFn(a, b) {
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
}
|
||||||
|
|
||||||
function searchComponent() {
|
function searchComponent() {
|
||||||
return {
|
return {
|
||||||
search: '',
|
search: '',
|
||||||
@@ -184,76 +259,90 @@
|
|||||||
mysqls: @js($mysqls),
|
mysqls: @js($mysqls),
|
||||||
mariadbs: @js($mariadbs),
|
mariadbs: @js($mariadbs),
|
||||||
services: @js($services),
|
services: @js($services),
|
||||||
|
gotoTag(tag) {
|
||||||
|
window.location.href = '/tags/' + tag;
|
||||||
|
},
|
||||||
|
goto(item) {
|
||||||
|
const hrefLink = item.hrefLink;
|
||||||
|
window.location.href = `${hrefLink}#tags`;
|
||||||
|
},
|
||||||
get filteredApplications() {
|
get filteredApplications() {
|
||||||
if (this.search === '') {
|
if (this.search === '') {
|
||||||
return this.applications;
|
return Object.values(this.applications).sort(sortFn);
|
||||||
}
|
}
|
||||||
this.applications = Object.values(this.applications);
|
this.applications = Object.values(this.applications);
|
||||||
return this.applications.filter(item => {
|
return this.applications.filter(item => {
|
||||||
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
|
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
|
||||||
item.fqdn?.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() {
|
get filteredPostgresqls() {
|
||||||
if (this.search === '') {
|
if (this.search === '') {
|
||||||
return this.postgresqls;
|
return Object.values(this.postgresqls).sort(sortFn);
|
||||||
}
|
}
|
||||||
this.postgresqls = Object.values(this.postgresqls);
|
this.postgresqls = Object.values(this.postgresqls);
|
||||||
return this.postgresqls.filter(item => {
|
return this.postgresqls.filter(item => {
|
||||||
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
|
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() {
|
get filteredRedis() {
|
||||||
if (this.search === '') {
|
if (this.search === '') {
|
||||||
return this.redis;
|
return Object.values(this.redis).sort(sortFn);
|
||||||
}
|
}
|
||||||
this.redis = Object.values(this.redis);
|
this.redis = Object.values(this.redis);
|
||||||
return this.redis.filter(item => {
|
return this.redis.filter(item => {
|
||||||
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
|
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() {
|
get filteredMongodbs() {
|
||||||
if (this.search === '') {
|
if (this.search === '') {
|
||||||
return this.mongodbs;
|
return Object.values(this.mongodbs).sort(sortFn);
|
||||||
}
|
}
|
||||||
this.mongodbs = Object.values(this.mongodbs);
|
this.mongodbs = Object.values(this.mongodbs);
|
||||||
return this.mongodbs.filter(item => {
|
return this.mongodbs.filter(item => {
|
||||||
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
|
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() {
|
get filteredMysqls() {
|
||||||
if (this.search === '') {
|
if (this.search === '') {
|
||||||
return this.mysqls;
|
return Object.values(this.mysqls).sort(sortFn);
|
||||||
}
|
}
|
||||||
this.mysqls = Object.values(this.mysqls);
|
this.mysqls = Object.values(this.mysqls);
|
||||||
return this.mysqls.filter(item => {
|
return this.mysqls.filter(item => {
|
||||||
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
|
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() {
|
get filteredMariadbs() {
|
||||||
if (this.search === '') {
|
if (this.search === '') {
|
||||||
return this.mariadbs;
|
return Object.values(this.mariadbs).sort(sortFn);
|
||||||
}
|
}
|
||||||
this.mariadbs = Object.values(this.mariadbs);
|
this.mariadbs = Object.values(this.mariadbs);
|
||||||
return this.mariadbs.filter(item => {
|
return this.mariadbs.filter(item => {
|
||||||
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
|
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() {
|
get filteredServices() {
|
||||||
if (this.search === '') {
|
if (this.search === '') {
|
||||||
return this.services;
|
return Object.values(this.services).sort(sortFn);
|
||||||
}
|
}
|
||||||
this.services = Object.values(this.services);
|
this.services = Object.values(this.services);
|
||||||
return this.services.filter(item => {
|
return this.services.filter(item => {
|
||||||
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
|
return item.name.toLowerCase().includes(this.search.toLowerCase()) ||
|
||||||
item.description?.toLowerCase().includes(this.search.toLowerCase());
|
item.description?.toLowerCase().includes(this.search.toLowerCase()) ||
|
||||||
});
|
item.tags?.some(tag => tag.name.toLowerCase().includes(this.search.toLowerCase()));
|
||||||
|
}).sort(sortFn);
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,6 +30,9 @@
|
|||||||
@click.prevent="activeTab = 'resource-operations'; window.location.hash = 'resource-operations'"
|
@click.prevent="activeTab = 'resource-operations'; window.location.hash = 'resource-operations'"
|
||||||
href="#">Resource Operations
|
href="#">Resource Operations
|
||||||
</a>
|
</a>
|
||||||
|
<a :class="activeTab === 'tags' && 'text-white'"
|
||||||
|
@click.prevent="activeTab = 'tags'; window.location.hash = 'tags'" href="#">Tags
|
||||||
|
</a>
|
||||||
<a :class="activeTab === 'danger' && 'text-white'"
|
<a :class="activeTab === 'danger' && 'text-white'"
|
||||||
@click.prevent="activeTab = 'danger';
|
@click.prevent="activeTab = 'danger';
|
||||||
window.location.hash = 'danger'"
|
window.location.hash = 'danger'"
|
||||||
@@ -164,6 +167,9 @@
|
|||||||
<div x-cloak x-show="activeTab === 'resource-operations'">
|
<div x-cloak x-show="activeTab === 'resource-operations'">
|
||||||
<livewire:project.shared.resource-operations :resource="$service" />
|
<livewire:project.shared.resource-operations :resource="$service" />
|
||||||
</div>
|
</div>
|
||||||
|
<div x-cloak x-show="activeTab === 'tags'">
|
||||||
|
<livewire:project.shared.tags :resource="$service" />
|
||||||
|
</div>
|
||||||
<div x-cloak x-show="activeTab === 'danger'">
|
<div x-cloak x-show="activeTab === 'danger'">
|
||||||
<livewire:project.shared.danger :resource="$service" />
|
<livewire:project.shared.danger :resource="$service" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,10 +11,15 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="grid grid-cols-1 gap-2 pb-4 lg:grid-cols-4">
|
<div class="grid grid-cols-1 gap-2 pb-4 lg:grid-cols-4">
|
||||||
@foreach ($server->destinations() as $destination)
|
@foreach ($server->destinations() as $destination)
|
||||||
<div class="flex flex-col gap-2 box" wire:click="cloneTo('{{ data_get($destination, 'id') }}')">
|
<x-new-modal action="cloneTo({{ data_get($destination, 'id') }})">
|
||||||
<div class="font-bold text-white">{{ $server->name }}</div>
|
<x:slot name="content">
|
||||||
<div>{{ $destination->name }}</div>
|
<div class="flex flex-col gap-2 box">
|
||||||
</div>
|
<div class="font-bold text-white">{{ $server->name }}</div>
|
||||||
|
<div>{{ $destination->name }}</div>
|
||||||
|
</div>
|
||||||
|
</x:slot>
|
||||||
|
<div>You are about to clone this resource.</div>
|
||||||
|
</x-new-modal>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,10 +37,15 @@
|
|||||||
@forelse ($projects as $project)
|
@forelse ($projects as $project)
|
||||||
<div class="flex flex-row flex-wrap gap-2">
|
<div class="flex flex-row flex-wrap gap-2">
|
||||||
@foreach ($project->environments as $environment)
|
@foreach ($project->environments as $environment)
|
||||||
<div class="flex flex-col gap-2 box" wire:click="moveTo('{{ data_get($environment, 'id') }}')">
|
<x-new-modal action="moveTo({{ data_get($environment, 'id') }})">
|
||||||
<div class="font-bold text-white">{{ $project->name }}</div>
|
<x:slot name="content">
|
||||||
<div><span class="text-warning">{{ $environment->name }}</span> environment</div>
|
<div class="flex flex-col gap-2 box">
|
||||||
</div>
|
<div class="font-bold text-white">{{ $project->name }}</div>
|
||||||
|
<div><span class="text-warning">{{ $environment->name }}</span> environment</div>
|
||||||
|
</div>
|
||||||
|
</x:slot>
|
||||||
|
<div>You are about to move this resource.</div>
|
||||||
|
</x-new-modal>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
@empty
|
@empty
|
||||||
|
|||||||
35
resources/views/livewire/project/shared/tags.blade.php
Normal file
35
resources/views/livewire/project/shared/tags.blade.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<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 }}')"
|
||||||
|
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>
|
||||||
|
@if ($tags->count() > 0)
|
||||||
|
<h3 class="pt-4">Already defined tags</h3>
|
||||||
|
<div>Click to quickly add one.</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>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
14
resources/views/livewire/tags/index.blade.php
Normal file
14
resources/views/livewire/tags/index.blade.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<div>
|
||||||
|
<h1>Tags</h1>
|
||||||
|
<div class="flex flex-col gap-2 pb-6 ">
|
||||||
|
<div>Available tags: </div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
@forelse ($tags as $oneTag)
|
||||||
|
<a class="flex items-center justify-center h-6 px-2 text-white min-w-14 w-fit hover:no-underline hover:bg-coolgray-200 bg-coolgray-100"
|
||||||
|
href="{{ route('tags.show', ['tag_name' => $oneTag->name]) }}">{{ $oneTag->name }}</a>
|
||||||
|
@empty
|
||||||
|
<div>No tags yet defined yet. Go to a resource and add a tag there.</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
80
resources/views/livewire/tags/show.blade.php
Normal file
80
resources/views/livewire/tags/show.blade.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<div>
|
||||||
|
<div class="flex items-start gap-2">
|
||||||
|
<div>
|
||||||
|
<h1>Tags</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 pb-6 ">
|
||||||
|
<div>Available tags: </div>
|
||||||
|
<div class="flex flex-wrap gap-2 ">
|
||||||
|
@forelse ($tags as $oneTag)
|
||||||
|
<a :class="{{ $tag->id == $oneTag->id }} && 'bg-coollabs hover:bg-coollabs-100'"
|
||||||
|
class="flex items-center justify-center h-6 px-2 text-white min-w-14 w-fit hover:no-underline hover:bg-coolgray-200 bg-coolgray-100"
|
||||||
|
href="{{ route('tags.show', ['tag_name' => $oneTag->name]) }}">{{ $oneTag->name }}</a>
|
||||||
|
@empty
|
||||||
|
<div>No tags yet defined yet. Go to a resource and add a tag there.</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="py-4">Details</h3>
|
||||||
|
<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 isHighlighted buttonTitle="Redeploy All" action="redeploy_all">
|
||||||
|
All resources will be redeployed.
|
||||||
|
</x-new-modal>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 gap-2 pt-4 lg:grid-cols-2 xl:grid-cols-3">
|
||||||
|
@foreach ($applications as $application)
|
||||||
|
<a href="{{ $application->link() }}" class="flex flex-col box group">
|
||||||
|
<span
|
||||||
|
class="font-bold text-white">{{ $application->project()->name }}/{{ $application->environment->name }}</span>
|
||||||
|
<span class="text-white ">{{ $application->name }}</span>
|
||||||
|
<span class="description">{{ $application->description }}</span>
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
@foreach ($services as $service)
|
||||||
|
<a href="{{ $service->link() }}" class="flex flex-col box group">
|
||||||
|
<span
|
||||||
|
class="font-bold text-white">{{ $service->project()->name }}/{{ $service->environment->name }}</span>
|
||||||
|
<span class="text-white ">{{ $service->name }}</span>
|
||||||
|
<span class="description">{{ $service->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>
|
||||||
112
routes/api.php
112
routes/api.php
@@ -6,7 +6,9 @@ use App\Actions\Database\StartMysql;
|
|||||||
use App\Actions\Database\StartPostgresql;
|
use App\Actions\Database\StartPostgresql;
|
||||||
use App\Actions\Database\StartRedis;
|
use App\Actions\Database\StartRedis;
|
||||||
use App\Actions\Service\StartService;
|
use App\Actions\Service\StartService;
|
||||||
|
use App\Http\Controllers\Api\Deploy;
|
||||||
use App\Models\ApplicationDeploymentQueue;
|
use App\Models\ApplicationDeploymentQueue;
|
||||||
|
use App\Models\Tag;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Providers\RouteServiceProvider;
|
use App\Providers\RouteServiceProvider;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -32,105 +34,25 @@ if (isDev()) {
|
|||||||
Route::get('/health', function () {
|
Route::get('/health', function () {
|
||||||
return 'OK';
|
return 'OK';
|
||||||
});
|
});
|
||||||
Route::group([
|
// Route::group([
|
||||||
'middleware' => $middlewares,
|
// 'middleware' => $middlewares,
|
||||||
'prefix' => 'v1'
|
// 'prefix' => 'v1'
|
||||||
], function () {
|
// ], function () {
|
||||||
Route::get('/deployments', function() {
|
// Route::get('/deployments', function () {
|
||||||
return ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->get([
|
// return ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->get([
|
||||||
"id",
|
// "id",
|
||||||
"server_id",
|
// "server_id",
|
||||||
"status"
|
// "status"
|
||||||
])->groupBy("server_id")->map(function($item) {
|
// ])->groupBy("server_id")->map(function ($item) {
|
||||||
return $item;
|
// return $item;
|
||||||
})->toArray();
|
// })->toArray();
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
Route::group([
|
Route::group([
|
||||||
'middleware' => ['auth:sanctum'],
|
'middleware' => ['auth:sanctum'],
|
||||||
'prefix' => 'v1'
|
'prefix' => 'v1'
|
||||||
], function () {
|
], function () {
|
||||||
Route::get('/deploy', function (Request $request) {
|
Route::get('/deploy', [Deploy::class, 'deploy']);
|
||||||
$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::middleware(['throttle:5'])->group(function () {
|
Route::middleware(['throttle:5'])->group(function () {
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ use App\Livewire\Server\Proxy\Logs as ProxyLogs;
|
|||||||
|
|
||||||
use App\Livewire\Source\Github\Change as GitHubChange;
|
use App\Livewire\Source\Github\Change as GitHubChange;
|
||||||
use App\Livewire\Subscription\Index as SubscriptionIndex;
|
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\TeamSharedVariablesIndex;
|
||||||
use App\Livewire\Waitlist\Index as WaitlistIndex;
|
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('/settings/license', SettingsLicense::class)->name('settings.license');
|
||||||
|
|
||||||
Route::get('/profile', ProfileIndex::class)->name('profile');
|
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::prefix('team')->group(function () {
|
||||||
Route::get('/', TeamIndex::class)->name('team.index');
|
Route::get('/', TeamIndex::class)->name('team.index');
|
||||||
Route::get('/new', TeamCreate::class)->name('team.create');
|
Route::get('/new', TeamCreate::class)->name('team.create');
|
||||||
|
|||||||
@@ -824,8 +824,12 @@ Route::post('/payments/stripe/events', function () {
|
|||||||
if (!$team) {
|
if (!$team) {
|
||||||
throw new Exception('No team found for subscription: ' . $subscription->id);
|
throw new Exception('No team found for subscription: ' . $subscription->id);
|
||||||
}
|
}
|
||||||
SubscriptionInvoiceFailedJob::dispatch($team);
|
if (!$subscription->stripe_invoice_paid) {
|
||||||
send_internal_notification('Invoice payment failed: ' . $subscription->team->id);
|
SubscriptionInvoiceFailedJob::dispatch($team);
|
||||||
|
send_internal_notification('Invoice payment failed: ' . $subscription->team->id);
|
||||||
|
} else {
|
||||||
|
send_internal_notification('Invoice payment failed but already paid: ' . $subscription->team->id);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'payment_intent.payment_failed':
|
case 'payment_intent.payment_failed':
|
||||||
$customerId = data_get($data, 'customer');
|
$customerId = data_get($data, 'customer');
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"version": "3.12.36"
|
"version": "3.12.36"
|
||||||
},
|
},
|
||||||
"v4": {
|
"v4": {
|
||||||
"version": "4.0.0-beta.204"
|
"version": "4.0.0-beta.206"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user